SidewalkGenerator.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import {
  2. ExtrudeGeometry,
  3. Group,
  4. InstancedMesh,
  5. MeshStandardNodeMaterial,
  6. Shape
  7. } from 'three/webgpu';
  8. import { cameraPosition, color, float, floor, Fn, fract, fwidth, If, mix, mx_noise_float, normalView, normalWorldGeometry, positionView, positionWorld, sin, smoothstep } from 'three/tsl';
  9. /**
  10. * Generates the raised sidewalk for a city's blocks: per block, a rounded-corner concrete
  11. * slab rimmed by a distinct granite kerbstone that stands proud of the walking surface and
  12. * drops to the road. Instanced across a list of placements and dressed with its own
  13. * procedural material ( poured concrete flags, scored expansion joints, granite curb ).
  14. * Returns a `THREE.Group` of two instanced meshes — the walking slab and the curb.
  15. *
  16. * Unlike the building generator, this one owns its materials: the slab and curb
  17. * geometry and the TSL that shades them live together here.
  18. *
  19. * ```js
  20. * const sidewalk = new SidewalkGenerator( { width: 90, depth: 60, height: 0.5 } );
  21. * scene.add( sidewalk.build( placements ) ); // placements: Matrix4[]
  22. * ```
  23. */
  24. class SidewalkGenerator {
  25. constructor( parameters = {} ) {
  26. this.parameters = Object.assign( {}, SidewalkGenerator.defaults, parameters );
  27. this.material = null; // the procedural concrete, built once and reused across rebuilds
  28. this.curbMaterial = null; // the procedural granite curb, likewise
  29. this.mesh = null;
  30. }
  31. build( placements ) {
  32. this.dispose();
  33. const { width, depth, height, radius, curbWidth, curbLip } = this.parameters;
  34. if ( this.material === null ) this.material = createSidewalkMaterial();
  35. if ( this.curbMaterial === null ) this.curbMaterial = createCurbMaterial();
  36. // the walking slab and the curb are separate meshes so each carries its own material
  37. const slab = new InstancedMesh( slabGeometry( width, depth, height, radius, curbWidth ), this.material, placements.length );
  38. const curb = new InstancedMesh( curbGeometry( width, depth, height, radius, curbWidth, curbLip ), this.curbMaterial, placements.length );
  39. for ( let i = 0; i < placements.length; i ++ ) {
  40. slab.setMatrixAt( i, placements[ i ] );
  41. curb.setMatrixAt( i, placements[ i ] );
  42. }
  43. slab.computeBoundingSphere();
  44. curb.computeBoundingSphere();
  45. slab.receiveShadow = curb.receiveShadow = true;
  46. const group = new Group();
  47. group.name = 'Sidewalk';
  48. group.add( slab, curb );
  49. this.mesh = group;
  50. return group;
  51. }
  52. dispose() {
  53. if ( this.mesh === null ) return;
  54. this.mesh.traverse( ( o ) => o.geometry && o.geometry.dispose() );
  55. this.mesh = null;
  56. }
  57. }
  58. SidewalkGenerator.defaults = {
  59. width: 90, // the block footprint each slab covers
  60. depth: 60,
  61. height: 0.5, // walking-surface height above the road
  62. radius: 5, // corner radius, so the sidewalk turns each intersection instead of a hard 90°
  63. curbWidth: 0.13, // top width of the granite kerbstone rimming the block ( ~5 in )
  64. curbLip: 0.01 // how far the curb stands proud of the walking surface ( near-flush )
  65. };
  66. // --- geometry ------------------------------------------------------------
  67. // the block footprint as a rounded-corner rectangle ( centred at the origin ), so the
  68. // sidewalk turns each intersection instead of meeting the kerb at a hard 90°
  69. function roundedRect( width, depth, radius ) {
  70. const w = width / 2;
  71. const d = depth / 2;
  72. const r = Math.min( radius, w, d );
  73. const shape = new Shape();
  74. shape.moveTo( - w + r, - d );
  75. shape.lineTo( w - r, - d );
  76. shape.quadraticCurveTo( w, - d, w, - d + r );
  77. shape.lineTo( w, d - r );
  78. shape.quadraticCurveTo( w, d, w - r, d );
  79. shape.lineTo( - w + r, d );
  80. shape.quadraticCurveTo( - w, d, - w, d - r );
  81. shape.lineTo( - w, - d + r );
  82. shape.quadraticCurveTo( - w, - d, - w + r, - d );
  83. return shape;
  84. }
  85. // extrude a footprint outline up by `height` ( the extrusion runs +Z; stand it up so height is +Y )
  86. function extrudeUp( shape, height ) {
  87. const geometry = new ExtrudeGeometry( shape, { depth: height, bevelEnabled: false, curveSegments: 6 } );
  88. geometry.rotateX( - Math.PI / 2 );
  89. return geometry;
  90. }
  91. // the walking slab: the inner concrete surface, inset to sit inside the curb and overlapping
  92. // it slightly so the seam is buried. base at y = 0, walking surface at `height`.
  93. function slabGeometry( width, depth, height, radius, curbWidth ) {
  94. const innerRadius = Math.max( 0.5, radius - curbWidth );
  95. return extrudeUp( roundedRect( width - 2 * curbWidth + 0.06, depth - 2 * curbWidth + 0.06, innerRadius ), height );
  96. }
  97. // the curb: a distinct full-height kerbstone band rimming the block ( the outline with an
  98. // inset hole ), standing proud of the walking slab by `curbLip` and dropping to the road.
  99. function curbGeometry( width, depth, height, radius, curbWidth, curbLip ) {
  100. const innerRadius = Math.max( 0.5, radius - curbWidth );
  101. const shape = roundedRect( width, depth, radius );
  102. shape.holes.push( roundedRect( width - 2 * curbWidth, depth - 2 * curbWidth, innerRadius ) );
  103. return extrudeUp( shape, height + curbLip );
  104. }
  105. // --- material ------------------------------------------------------------
  106. // derivative-based bump for a procedural, world-space height field. the built-in bumpMap
  107. // offsets the UV to read its height, so it returns a zero gradient for a height keyed off
  108. // world position; this feeds the hardware screen-space derivatives of the height into
  109. // Mikkelsen's surface-gradient method so the relief actually perturbs the normal.
  110. function bumpNormal( height ) {
  111. const dpdx = positionView.dFdx();
  112. const dpdy = positionView.dFdy();
  113. const r1 = dpdy.cross( normalView );
  114. const r2 = normalView.cross( dpdx );
  115. const det = dpdx.dot( r1 );
  116. const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) );
  117. return det.abs().mul( normalView ).sub( grad ).normalize();
  118. }
  119. // an antialiased line repeated at every multiple of `period` ( the scored joints )
  120. function gridLine( coord, period, halfWidth ) {
  121. const g = coord.div( period );
  122. const d = float( 0.5 ).sub( fract( g ).sub( 0.5 ).abs() ); // distance to nearest line, in periods
  123. const aa = fwidth( g ).max( 0.0001 );
  124. const hw = halfWidth / period;
  125. return smoothstep( float( hw ).add( aa ), float( hw ).sub( aa ), d );
  126. }
  127. // a noise term that only resolves up close: sampled inside a detail branch ( and kept in its
  128. // own single-output Fn, so it is evaluated only in the output flow that consumes it )
  129. function detailNoise( p, detail, scale, amp ) {
  130. return Fn( () => {
  131. const n = float( 0 ).toVar();
  132. If( detail.greaterThan( 0 ), () => {
  133. n.assign( mx_noise_float( p.mul( scale ) ).mul( amp ) );
  134. } );
  135. return n;
  136. } )();
  137. }
  138. function createSidewalkMaterial() {
  139. // concrete flags: each poured slab a slightly different tone, fine aggregate speckle
  140. // and expansion joints scored on a grid both ways
  141. const p = positionWorld;
  142. const detail = smoothstep( 200, 18, p.distance( cameraPosition ) );
  143. const panel = 1.5; // flag size ( ~5 ft NYC sidewalk flags )
  144. const panelHash = fract( sin( floor( p.x.div( panel ) ).mul( 127.1 ).add( floor( p.z.div( panel ) ).mul( 311.7 ) ) ).mul( 43758.5453 ) );
  145. const tone = mx_noise_float( p.mul( 0.5 ) ).mul( 0.5 ).add( 0.5 );
  146. // fine aggregate speckle ( grit, tinting the colour ) and grain relief ( driving the normal )
  147. const grit = detailNoise( p, detail, 14, 0.07 ).mul( detail );
  148. const grain = detailNoise( p, detail, 3, 0.003 );
  149. const base = mix( color( 0x6f6f68 ), color( 0x8c8c82 ), tone ).mul( panelHash.sub( 0.5 ).mul( 0.16 ).add( 1 ) ); // per-flag tone
  150. const concrete = base.add( grit );
  151. const joints = gridLine( p.x, panel, 0.045 ).max( gridLine( p.z, panel, 0.045 ) ).mul( detail );
  152. const material = new MeshStandardNodeMaterial();
  153. material.colorNode = concrete.mul( joints.mul( 0.45 ).oneMinus() );
  154. material.roughnessNode = float( 0.92 ).sub( panelHash.mul( 0.05 ) );
  155. material.normalNode = bumpNormal( grain.sub( joints.mul( 0.012 ) ).mul( detail ) ); // world units: ~3 mm grain, ~12 mm scored joints
  156. return material;
  157. }
  158. function createCurbMaterial() {
  159. // granite kerbstone: a dense, cool grey stone — darker and smoother than the concrete
  160. // flags — with a fine speckle, segment joints every ~1.5 m and a grimier road-facing face
  161. const p = positionWorld;
  162. const detail = smoothstep( 200, 18, p.distance( cameraPosition ) );
  163. const tone = mx_noise_float( p.mul( 0.6 ) ).mul( 0.5 ).add( 0.5 );
  164. const stone = mix( color( 0x46463f ), color( 0x5c5c54 ), tone ).add( detailNoise( p, detail, 18, 0.05 ).mul( detail ) ); // dark cool granite, fine speckle
  165. const seg = 1.5; // kerbstone segment length
  166. const joints = gridLine( p.x, seg, 0.04 ).max( gridLine( p.z, seg, 0.04 ) ).mul( detail );
  167. const top = smoothstep( 0.5, 0.85, normalWorldGeometry.y ); // 1 on the curb top, 0 on its walls
  168. const dressed = mix( stone.mul( 0.7 ), stone, top ).mul( joints.mul( 0.4 ).oneMinus() ); // grimier on the road-facing face
  169. const material = new MeshStandardNodeMaterial();
  170. material.colorNode = dressed;
  171. material.roughnessNode = float( 0.7 ).add( tone.mul( 0.1 ) ); // flamed granite: matte, a touch smoother than the concrete sidewalk
  172. material.normalNode = bumpNormal( detailNoise( p, detail, 4, 0.002 ).mul( detail ) ); // fine granite grain
  173. return material;
  174. }
  175. export { SidewalkGenerator };
粤ICP备19079148号