From a72e673b300a9aa1ca7723d06bd912468e78cecb Mon Sep 17 00:00:00 2001 From: mrdoob Date: Wed, 24 Jun 2026 18:38:38 +0800 Subject: [PATCH 1/2] Examples: Add procedural city generator. (#33817) Co-authored-by: Claude Opus 4.8 (1M context) --- examples/files.json | 2 + examples/jsm/generators/CityGenerator.js | 346 +++++ .../jsm/generators/city/SidewalkGenerator.js | 253 +++ .../generators/city/SkyscraperGenerator.js | 1357 +++++++++++++++++ .../screenshots/webgpu_generator_building.jpg | Bin 0 -> 17699 bytes .../screenshots/webgpu_generator_city.jpg | Bin 0 -> 75918 bytes examples/webgpu_generator_building.html | 276 ++++ examples/webgpu_generator_city.html | 225 +++ test/e2e/puppeteer.js | 5 +- 9 files changed, 2463 insertions(+), 1 deletion(-) create mode 100644 examples/jsm/generators/CityGenerator.js create mode 100644 examples/jsm/generators/city/SidewalkGenerator.js create mode 100644 examples/jsm/generators/city/SkyscraperGenerator.js create mode 100644 examples/screenshots/webgpu_generator_building.jpg create mode 100644 examples/screenshots/webgpu_generator_city.jpg create mode 100644 examples/webgpu_generator_building.html create mode 100644 examples/webgpu_generator_city.html diff --git a/examples/files.json b/examples/files.json index 318161ba0a3e25..e6b6e3574f0acb 100644 --- a/examples/files.json +++ b/examples/files.json @@ -344,6 +344,8 @@ "webgpu_equirectangular", "webgpu_fog_height", "webgpu_furnace_test", + "webgpu_generator_building", + "webgpu_generator_city", "webgpu_geometry_loft", "webgpu_hdr", "webgpu_instance_mesh", diff --git a/examples/jsm/generators/CityGenerator.js b/examples/jsm/generators/CityGenerator.js new file mode 100644 index 00000000000000..627c3e981a40d9 --- /dev/null +++ b/examples/jsm/generators/CityGenerator.js @@ -0,0 +1,346 @@ +import { + Group, + Matrix4 +} from 'three'; + +import { MeshStandardNodeMaterial } from 'three/webgpu'; +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'; + +import { SkyscraperGenerator, createSkyscraperMaterial, buildingPalette } from './city/SkyscraperGenerator.js'; +import { SidewalkGenerator } from './city/SidewalkGenerator.js'; + +/** + * Lays out a grid of city blocks and fills each lot with a {@link SkyscraperGenerator} + * tower of its own seed, height and footprint, optionally on raised sidewalk + * slabs (curbs). Returns a `THREE.Group` ready to add to a scene. + * + * Pass a building material to dress the towers; the sidewalks dress themselves + * via {@link SidewalkGenerator}. The layout is exposed as + * {@link CityGenerator#layout} so the surrounding scene (road markings, etc.) + * can align to the same grid. + * + * ```js + * const city = new CityGenerator( { seed: 1 } ); + * scene.add( city.build( materials ) ); + * ``` + */ +class CityGenerator { + + constructor( parameters = {} ) { + + this.parameters = Object.assign( {}, CityGenerator.defaults, parameters ); + this.layout = cityLayout( this.parameters ); + + this.generators = []; + this.sidewalk = new SidewalkGenerator( { + width: this.layout.blockW, + depth: this.layout.blockD, + height: this.parameters.curbHeight, + radius: this.parameters.curbRadius + } ); + this.group = null; + + } + + build( materials = {} ) { + + this.dispose(); + + const group = new Group(); + group.name = 'City'; + + const L = this.layout; + const random = createRandom( this.parameters.seed ); + + // raise the lots onto rounded sidewalk slabs ( curbs ) when curbHeight > 0 + + const curb = this.parameters.curbHeight; + const slabs = []; + + for ( let bx = 0; bx < L.blocksX; bx ++ ) { + + for ( let bz = 0; bz < L.blocksZ; bz ++ ) { + + const blockX = - L.cityW / 2 + bx * ( L.blockW + L.street ); + const blockZ = - L.cityD / 2 + bz * ( L.blockD + L.street ); + + if ( curb > 0 ) { + + slabs.push( new Matrix4().makeTranslation( blockX + L.blockW / 2, 0, blockZ + L.blockD / 2 ) ); + + } + + for ( let lx = 0; lx < L.lotsX; lx ++ ) { + + for ( let lz = 0; lz < L.lotsZ; lz ++ ) { + + // a chamfered corner only reads as architecture when it faces the + // block's corner ( the street intersection ), so only the four corner + // lots are cut, each toward its own outward corner; the rest stay square + const cornerX = lx === 0 ? - 1 : ( lx === L.lotsX - 1 ? 1 : 0 ); + const cornerZ = lz === 0 ? - 1 : ( lz === L.lotsZ - 1 ? 1 : 0 ); + const onCorner = cornerX !== 0 && cornerZ !== 0; + + const tall = random(); + + const generator = new SkyscraperGenerator( { + seed: Math.floor( random() * 100000 ), + totalHeight: 38 + tall * tall * 114, // a few tall towers, mostly mid-rise + 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 + floorHeight: 3.4 + random() * 1.8, + bayWidth: 1.9 + random() * 2.1, + pierWidth: 0.4 + random() * 0.5, + pierDepth: 0.3 + random() * 0.4, + chamferWidth: onCorner ? 3 + random() * 4 : 0, + chamferCornerX: cornerX, + chamferCornerZ: cornerZ, + setbackDepth: random() < 0.4 ? 0.8 + random() * 2 : 0, // only some towers step back at the crown; the rest rise flat + stringCourseEvery: random() < 0.85 ? 3 + Math.floor( random() * 6 ) : 0 + }, materials.building ); + + const building = generator.build(); + building.position.set( blockX + ( lx + 0.5 ) * L.lot, curb, blockZ + ( lz + 0.5 ) * L.lot ); + building.castShadow = building.receiveShadow = true; + + group.add( building ); + this.generators.push( generator ); + + } + + } + + } + + } + + if ( slabs.length > 0 ) group.add( this.sidewalk.build( slabs ) ); + + this.group = group; + + return group; + + } + + dispose() { + + for ( const generator of this.generators ) generator.dispose(); + this.generators.length = 0; + + this.sidewalk.dispose(); + + this.group = null; + + } + +} + +CityGenerator.defaults = { + seed: 1, + street: 22, + lot: 30, + lotsX: 3, + lotsZ: 2, + blocksX: 2, + blocksZ: 2, + curbHeight: 0.15, // ~6 in standard curb reveal / sidewalk height above the road + curbRadius: 5 +}; + +// derives the block / street dimensions from the parameters +function cityLayout( parameters ) { + + const { street, lot, lotsX, lotsZ, blocksX, blocksZ } = parameters; + + const blockW = lotsX * lot; + const blockD = lotsZ * lot; + + return { + street, lot, lotsX, lotsZ, blocksX, blocksZ, blockW, blockD, + cityW: blocksX * blockW + ( blocksX - 1 ) * street, + cityD: blocksZ * blockD + ( blocksZ - 1 ) * street + }; + +} + +// deterministic PRNG (mulberry32) so a seed always lays out the same city +function createRandom( seed ) { + + let s = ( seed >>> 0 ) || 1; + + return function () { + + s = ( s + 0x6D2B79F5 ) | 0; + let t = Math.imul( s ^ ( s >>> 15 ), 1 | s ); + t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t; + return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296; + + }; + +} + +// --- road material ------------------------------------------------------- + +// derivative-based bump for a procedural, world-space height field. the built-in bumpMap +// offsets the UV to read its height, so it returns a zero gradient for a height keyed off +// world position; this feeds the hardware screen-space derivatives of the height into +// Mikkelsen's surface-gradient method so the relief actually perturbs the normal. +function bumpNormal( height ) { + + const dpdx = positionView.dFdx(); + const dpdy = positionView.dFdy(); + const r1 = dpdy.cross( normalView ); + const r2 = normalView.cross( dpdx ); + const det = dpdx.dot( r1 ); + const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) ); + + return det.abs().mul( normalView ).sub( grad ).normalize(); + +} + +// antialiased filled band: 1 where |coord| < halfWidth, edge sized to the +// pixel footprint ( fwidth ) so thin road paint stays crisp and doesn't shimmer +function lineAA( coord, halfWidth ) { + + const aa = fwidth( coord ).max( 0.0001 ); + return smoothstep( float( halfWidth ).add( aa ), float( halfWidth ).sub( aa ), coord.abs() ); + +} + +// the same, repeated at every multiple of `period` ( stripes, joints ) +function gridLine( coord, period, halfWidth ) { + + const g = coord.div( period ); + const d = float( 0.5 ).sub( fract( g ).sub( 0.5 ).abs() ); // distance to nearest line, in periods + const aa = fwidth( g ).max( 0.0001 ); + const hw = halfWidth / period; + return smoothstep( float( hw ).add( aa ), float( hw ).sub( aa ), d ); + +} + +/** + * The shared material every tower in a {@link CityGenerator} is dressed with: one flat + * masonry colour per lot, picked from a palette by hashing the lot's grid cell. + */ +function createBuildingMaterial( layout, seed = 0 ) { + + // every tower takes one flat colour, picked by hashing its lot — one shared material + // dresses the whole skyline; common tones repeat so the equal-probability pick feels real + const palette = buildingPalette.map( hex => color( hex ) ); + + const periodX = layout.blockW + layout.street; + const periodZ = layout.blockD + layout.street; + const gx = positionWorld.x.add( layout.cityW / 2 ); + const gz = positionWorld.z.add( layout.cityD / 2 ); + const blockIX = floor( gx.div( periodX ) ); + const blockIZ = floor( gz.div( periodZ ) ); + const cellX = blockIX.mul( layout.lotsX ).add( floor( gx.sub( blockIX.mul( periodX ) ).div( layout.lot ) ) ); + const cellZ = blockIZ.mul( layout.lotsZ ).add( floor( gz.sub( blockIZ.mul( periodZ ) ).div( layout.lot ) ) ); + const cellKey = uint( cellX.add( 4096 ) ).mul( uint( 73856093 ) ).bitXor( uint( cellZ.add( 4096 ) ).mul( uint( 19349663 ) ) ).bitXor( uint( ( seed * 2654435761 ) >>> 0 ) ).toVar(); + const cellHash = ( a, b ) => hash( cellKey.add( uint( Math.round( ( a + b * 7 ) * 100 ) ) ) ); + + const pick = cellHash( 127.1, 311.7 ); + let buildingBase = palette[ 0 ]; + for ( let i = 1; i < palette.length; i ++ ) buildingBase = mix( buildingBase, palette[ i ], step( i / palette.length, pick ) ); + buildingBase = buildingBase.mul( cellHash( 269.5, 183.3 ).mul( 0.12 ).add( 0.94 ) ); // subtle per-building brightness + + // the pick is constant across a tower, so resolve it once per vertex ( varying ) + return createSkyscraperMaterial( varying( buildingBase ) ); + +} + +/** + * The road surface: wet asphalt with lane lines and crosswalks aligned to a + * {@link CityGenerator} layout. Apply it to a ground plane sized to the city. + */ +function createRoadMaterial( layout ) { + + // wet asphalt: a warm-grey base in patchwork pours, two-scale aggregate + // grit, oily wear stains, hairline cracks and low-frequency wet patches + // that turn glossy and mirror the sky. detail fades in as the camera nears. + + const p = positionWorld; + const detail = smoothstep( 240, 25, p.distance( cameraPosition ) ); + + const blotch = mx_fractal_noise_float( p.mul( 0.2 ), 3 ).mul( 0.5 ).add( 0.5 ); + + // close-range detail — aggregate grit, oily wear pools, hairline cracks and worn + // paint — only resolves near the camera, so its noise is sampled ( inside the branch ) + // only where detail is non-zero and skipped across the far majority of the road + const near = Fn( () => { + + const grit = float( 0 ).toVar(); // two scales of aggregate, -1..1 + const stain = float( 0 ).toVar(); // oily wear pools + const crack = float( 0 ).toVar(); + const worn = float( 1 ).toVar(); // paint rubbed thin and patchy, more so where tyres cross it + + If( detail.greaterThan( 0 ), () => { + + grit.assign( mx_noise_float( p.mul( 7 ) ).add( mx_noise_float( p.mul( 23 ) ) ).mul( 0.5 ) ); + stain.assign( smoothstep( 0.5, 0.85, mx_fractal_noise_float( p.mul( 0.45 ), 3 ).mul( 0.5 ).add( 0.5 ) ) ); + crack.assign( smoothstep( 0.88, 1, mx_fractal_noise_float( p.mul( 1.1 ), 4 ).abs().oneMinus() ).mul( detail ) ); + 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 ) ); + + } ); + + return vec4( grit, stain, crack, worn ); + + } )(); + + const grit = near.x; + const stain = near.y; + const crack = near.z; + const worn = near.w; + + const base = mix( color( 0x24262b ), color( 0x3b3f46 ), blotch ); + const gritty = base.mul( grit.mul( 0.22 ).mul( detail ).add( 1 ) ); + const asphalt = mix( gritty, gritty.mul( 0.5 ), stain.mul( 0.5 ).mul( detail ) ); + + const wet = smoothstep( 0.6, 0.85, mx_fractal_noise_float( p.mul( 0.14 ), 2 ).mul( 0.5 ).add( 0.5 ) ); + + // markings, aligned to the block / street grid. fx, fz are the position + // within one block+street period; the street is the [ blockW, period ) part. + + const periodX = layout.blockW + layout.street; + const periodZ = layout.blockD + layout.street; + const fx = mod( p.x.add( layout.cityW / 2 ), periodX ); + const fz = mod( p.z.add( layout.cityD / 2 ), periodZ ); + const inStreetX = step( layout.blockW, fx ); // in a vertical street ( gap in X ) + const inStreetZ = step( layout.blockD, fz ); // in a horizontal street ( gap in Z ) + const su = fx.sub( layout.blockW ); // across the vertical street + const sv = fz.sub( layout.blockD ); // across the horizontal street + + // lane markings down each street ( not through intersections ): a solid + // centre line splitting the two directions, with a dashed divider in each + // half, so every street carries four lanes + const dashV = step( fract( p.z.div( 7 ) ), 0.5 ); + const dashH = step( fract( p.x.div( 7 ) ), 0.5 ); + + const centreV = lineAA( su.sub( layout.street / 2 ), 0.12 ); + const dividerV = lineAA( su.sub( layout.street / 4 ), 0.1 ).max( lineAA( su.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashV ); + const laneV = centreV.max( dividerV ).mul( inStreetX ).mul( inStreetZ.oneMinus() ); + + const centreH = lineAA( sv.sub( layout.street / 2 ), 0.12 ); + const dividerH = lineAA( sv.sub( layout.street / 4 ), 0.1 ).max( lineAA( sv.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashH ); + const laneH = centreH.max( dividerH ).mul( inStreetZ ).mul( inStreetX.oneMinus() ); + + // continental crosswalk bars ( long in the travel direction ) in each + // street arm, near the block edges it meets + const cw = 5; + const nearZ = step( fz, cw ).max( step( layout.blockD - cw, fz ) ); + const nearX = step( fx, cw ).max( step( layout.blockW - cw, fx ) ); + const crossV = gridLine( su, 1.2, 0.38 ).mul( inStreetX ).mul( inStreetZ.oneMinus() ).mul( nearZ ); + const crossH = gridLine( sv, 1.2, 0.38 ).mul( inStreetZ ).mul( inStreetX.oneMinus() ).mul( nearX ); + + const paint = laneV.max( laneH ).max( crossV ).max( crossH ).mul( detail ).mul( worn ); + + const material = new MeshStandardNodeMaterial(); + const surface = mix( asphalt, asphalt.mul( 0.6 ), wet ).mul( crack.mul( 0.5 ).oneMinus() ); + material.colorNode = mix( surface, color( 0xd0ccc0 ), paint ); // worn white paint + material.roughnessNode = mix( float( 0.95 ).sub( paint.mul( 0.2 ) ), float( 0.32 ), wet ); + material.normalNode = bumpNormal( grit.mul( 0.003 ).sub( crack.mul( 0.01 ) ).mul( detail ) ); // world units: ~3 mm aggregate, ~10 mm cracks + + return material; + +} + +export { CityGenerator, createBuildingMaterial, createRoadMaterial }; diff --git a/examples/jsm/generators/city/SidewalkGenerator.js b/examples/jsm/generators/city/SidewalkGenerator.js new file mode 100644 index 00000000000000..60cc26843347dd --- /dev/null +++ b/examples/jsm/generators/city/SidewalkGenerator.js @@ -0,0 +1,253 @@ +import { + ExtrudeGeometry, + Group, + InstancedMesh, + MeshStandardNodeMaterial, + Shape +} from 'three/webgpu'; + +import { cameraPosition, color, float, floor, Fn, fract, fwidth, If, mix, mx_noise_float, normalView, normalWorldGeometry, positionView, positionWorld, sin, smoothstep } from 'three/tsl'; + +/** + * Generates the raised sidewalk for a city's blocks: per block, a rounded-corner concrete + * slab rimmed by a distinct granite kerbstone that stands proud of the walking surface and + * drops to the road. Instanced across a list of placements and dressed with its own + * procedural material ( poured concrete flags, scored expansion joints, granite curb ). + * Returns a `THREE.Group` of two instanced meshes — the walking slab and the curb. + * + * Unlike the building generator, this one owns its materials: the slab and curb + * geometry and the TSL that shades them live together here. + * + * ```js + * const sidewalk = new SidewalkGenerator( { width: 90, depth: 60, height: 0.5 } ); + * scene.add( sidewalk.build( placements ) ); // placements: Matrix4[] + * ``` + */ +class SidewalkGenerator { + + constructor( parameters = {} ) { + + this.parameters = Object.assign( {}, SidewalkGenerator.defaults, parameters ); + + this.material = null; // the procedural concrete, built once and reused across rebuilds + this.curbMaterial = null; // the procedural granite curb, likewise + this.mesh = null; + + } + + build( placements ) { + + this.dispose(); + + const { width, depth, height, radius, curbWidth, curbLip } = this.parameters; + + if ( this.material === null ) this.material = createSidewalkMaterial(); + if ( this.curbMaterial === null ) this.curbMaterial = createCurbMaterial(); + + // the walking slab and the curb are separate meshes so each carries its own material + const slab = new InstancedMesh( slabGeometry( width, depth, height, radius, curbWidth ), this.material, placements.length ); + const curb = new InstancedMesh( curbGeometry( width, depth, height, radius, curbWidth, curbLip ), this.curbMaterial, placements.length ); + + for ( let i = 0; i < placements.length; i ++ ) { + + slab.setMatrixAt( i, placements[ i ] ); + curb.setMatrixAt( i, placements[ i ] ); + + } + + slab.computeBoundingSphere(); + curb.computeBoundingSphere(); + slab.receiveShadow = curb.receiveShadow = true; + + const group = new Group(); + group.name = 'Sidewalk'; + group.add( slab, curb ); + + this.mesh = group; + + return group; + + } + + dispose() { + + if ( this.mesh === null ) return; + + this.mesh.traverse( ( o ) => o.geometry && o.geometry.dispose() ); + this.mesh = null; + + } + +} + +SidewalkGenerator.defaults = { + width: 90, // the block footprint each slab covers + depth: 60, + height: 0.5, // walking-surface height above the road + radius: 5, // corner radius, so the sidewalk turns each intersection instead of a hard 90° + curbWidth: 0.13, // top width of the granite kerbstone rimming the block ( ~5 in ) + curbLip: 0.01 // how far the curb stands proud of the walking surface ( near-flush ) +}; + +// --- geometry ------------------------------------------------------------ + +// the block footprint as a rounded-corner rectangle ( centred at the origin ), so the +// sidewalk turns each intersection instead of meeting the kerb at a hard 90° +function roundedRect( width, depth, radius ) { + + const w = width / 2; + const d = depth / 2; + const r = Math.min( radius, w, d ); + + const shape = new Shape(); + shape.moveTo( - w + r, - d ); + shape.lineTo( w - r, - d ); + shape.quadraticCurveTo( w, - d, w, - d + r ); + shape.lineTo( w, d - r ); + shape.quadraticCurveTo( w, d, w - r, d ); + shape.lineTo( - w + r, d ); + shape.quadraticCurveTo( - w, d, - w, d - r ); + shape.lineTo( - w, - d + r ); + shape.quadraticCurveTo( - w, - d, - w + r, - d ); + + return shape; + +} + +// extrude a footprint outline up by `height` ( the extrusion runs +Z; stand it up so height is +Y ) +function extrudeUp( shape, height ) { + + const geometry = new ExtrudeGeometry( shape, { depth: height, bevelEnabled: false, curveSegments: 6 } ); + geometry.rotateX( - Math.PI / 2 ); + + return geometry; + +} + +// the walking slab: the inner concrete surface, inset to sit inside the curb and overlapping +// it slightly so the seam is buried. base at y = 0, walking surface at `height`. +function slabGeometry( width, depth, height, radius, curbWidth ) { + + const innerRadius = Math.max( 0.5, radius - curbWidth ); + return extrudeUp( roundedRect( width - 2 * curbWidth + 0.06, depth - 2 * curbWidth + 0.06, innerRadius ), height ); + +} + +// the curb: a distinct full-height kerbstone band rimming the block ( the outline with an +// inset hole ), standing proud of the walking slab by `curbLip` and dropping to the road. +function curbGeometry( width, depth, height, radius, curbWidth, curbLip ) { + + const innerRadius = Math.max( 0.5, radius - curbWidth ); + const shape = roundedRect( width, depth, radius ); + shape.holes.push( roundedRect( width - 2 * curbWidth, depth - 2 * curbWidth, innerRadius ) ); + return extrudeUp( shape, height + curbLip ); + +} + +// --- material ------------------------------------------------------------ + +// derivative-based bump for a procedural, world-space height field. the built-in bumpMap +// offsets the UV to read its height, so it returns a zero gradient for a height keyed off +// world position; this feeds the hardware screen-space derivatives of the height into +// Mikkelsen's surface-gradient method so the relief actually perturbs the normal. +function bumpNormal( height ) { + + const dpdx = positionView.dFdx(); + const dpdy = positionView.dFdy(); + const r1 = dpdy.cross( normalView ); + const r2 = normalView.cross( dpdx ); + const det = dpdx.dot( r1 ); + const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) ); + + return det.abs().mul( normalView ).sub( grad ).normalize(); + +} + +// an antialiased line repeated at every multiple of `period` ( the scored joints ) +function gridLine( coord, period, halfWidth ) { + + const g = coord.div( period ); + const d = float( 0.5 ).sub( fract( g ).sub( 0.5 ).abs() ); // distance to nearest line, in periods + const aa = fwidth( g ).max( 0.0001 ); + const hw = halfWidth / period; + return smoothstep( float( hw ).add( aa ), float( hw ).sub( aa ), d ); + +} + +// a noise term that only resolves up close: sampled inside a detail branch ( and kept in its +// own single-output Fn, so it is evaluated only in the output flow that consumes it ) +function detailNoise( p, detail, scale, amp ) { + + return Fn( () => { + + const n = float( 0 ).toVar(); + + If( detail.greaterThan( 0 ), () => { + + n.assign( mx_noise_float( p.mul( scale ) ).mul( amp ) ); + + } ); + + return n; + + } )(); + +} + +function createSidewalkMaterial() { + + // concrete flags: each poured slab a slightly different tone, fine aggregate speckle + // and expansion joints scored on a grid both ways + + const p = positionWorld; + const detail = smoothstep( 200, 18, p.distance( cameraPosition ) ); + + const panel = 1.5; // flag size ( ~5 ft NYC sidewalk flags ) + const panelHash = fract( sin( floor( p.x.div( panel ) ).mul( 127.1 ).add( floor( p.z.div( panel ) ).mul( 311.7 ) ) ).mul( 43758.5453 ) ); + + const tone = mx_noise_float( p.mul( 0.5 ) ).mul( 0.5 ).add( 0.5 ); + + // fine aggregate speckle ( grit, tinting the colour ) and grain relief ( driving the normal ) + const grit = detailNoise( p, detail, 14, 0.07 ).mul( detail ); + const grain = detailNoise( p, detail, 3, 0.003 ); + + const base = mix( color( 0x6f6f68 ), color( 0x8c8c82 ), tone ).mul( panelHash.sub( 0.5 ).mul( 0.16 ).add( 1 ) ); // per-flag tone + const concrete = base.add( grit ); + + const joints = gridLine( p.x, panel, 0.045 ).max( gridLine( p.z, panel, 0.045 ) ).mul( detail ); + + const material = new MeshStandardNodeMaterial(); + material.colorNode = concrete.mul( joints.mul( 0.45 ).oneMinus() ); + material.roughnessNode = float( 0.92 ).sub( panelHash.mul( 0.05 ) ); + material.normalNode = bumpNormal( grain.sub( joints.mul( 0.012 ) ).mul( detail ) ); // world units: ~3 mm grain, ~12 mm scored joints + + return material; + +} + +function createCurbMaterial() { + + // granite kerbstone: a dense, cool grey stone — darker and smoother than the concrete + // flags — with a fine speckle, segment joints every ~1.5 m and a grimier road-facing face + + const p = positionWorld; + const detail = smoothstep( 200, 18, p.distance( cameraPosition ) ); + + const tone = mx_noise_float( p.mul( 0.6 ) ).mul( 0.5 ).add( 0.5 ); + const stone = mix( color( 0x46463f ), color( 0x5c5c54 ), tone ).add( detailNoise( p, detail, 18, 0.05 ).mul( detail ) ); // dark cool granite, fine speckle + + const seg = 1.5; // kerbstone segment length + const joints = gridLine( p.x, seg, 0.04 ).max( gridLine( p.z, seg, 0.04 ) ).mul( detail ); + const top = smoothstep( 0.5, 0.85, normalWorldGeometry.y ); // 1 on the curb top, 0 on its walls + const dressed = mix( stone.mul( 0.7 ), stone, top ).mul( joints.mul( 0.4 ).oneMinus() ); // grimier on the road-facing face + + const material = new MeshStandardNodeMaterial(); + material.colorNode = dressed; + material.roughnessNode = float( 0.7 ).add( tone.mul( 0.1 ) ); // flamed granite: matte, a touch smoother than the concrete sidewalk + material.normalNode = bumpNormal( detailNoise( p, detail, 4, 0.002 ).mul( detail ) ); // fine granite grain + + return material; + +} + +export { SidewalkGenerator }; diff --git a/examples/jsm/generators/city/SkyscraperGenerator.js b/examples/jsm/generators/city/SkyscraperGenerator.js new file mode 100644 index 00000000000000..69c101e9530ded --- /dev/null +++ b/examples/jsm/generators/city/SkyscraperGenerator.js @@ -0,0 +1,1357 @@ +import { + BoxGeometry, + BufferAttribute, + BufferGeometry, + ExtrudeGeometry, + InterpolationSamplingMode, + InterpolationSamplingType, + LatheGeometry, + Matrix3, + Matrix4, + Mesh, + MeshStandardMaterial, + Path, + PlaneGeometry, + ShapeGeometry, + Shape, + Sphere, + Vector2, + Vector3 +} from 'three'; + +import { MeshStandardNodeMaterial } from 'three/webgpu'; +import { attribute, cameraPosition, color, cross, dot, float, floor, Fn, fract, fwidth, hash as ihash, mix, mod, modelWorldMatrixInverse, mx_fractal_noise_float, mx_noise_float, normalLocal, normalView, normalWorldGeometry, positionLocal, positionView, positionWorld, select, smoothstep, step, uint, uv, varying, vec2, vec3, vec4 } from 'three/tsl'; + +import { mergeGeometries } from '../../utils/BufferGeometryUtils.js'; + +const _scale = /*@__PURE__*/ new Vector3(); +const _point = /*@__PURE__*/ new Vector3(); +const _normalMatrix = /*@__PURE__*/ new Matrix3(); +const _identity = /*@__PURE__*/ new Matrix4(); + +// material-zone codes baked per vertex into the merged geometry, so one material can +// branch on partId and shade every zone +const PartId = { WALL: 0, PIER: 1, FRAME: 2, ORNAMENT: 3, GLASS: 4, AC: 5 }; +const { WALL, PIER, FRAME, ORNAMENT, GLASS, AC } = PartId; + +// fraction of a floor's height taken by the glazed opening; the remainder is +// the spandrel band. shared by the window module and the spandrels so they tile. +const WINDOW_HEIGHT_RATIO = 0.62; + +// width of the flat window-frame band around the glazing; shared by the frame module +// and the glass pane so the pane always tucks inside the frame +const WINDOW_BORDER = 0.1; + +// the masonry course module ( brick height × length ). the generator snaps floor and +// bay dimensions to it, and the material's coursing reads the same values, so the +// procedural brickwork lines up with the geometry +const BRICK = { height: 0.3, length: 0.6 }; + +// merging requires all-indexed or all-non-indexed inputs; extrusions are +// non-indexed while boxes/planes are indexed, so normalize before merging + +function merge( geometries ) { + + return mergeGeometries( geometries.map( ( g ) => g.index ? g.toNonIndexed() : g ) ); + +} + +function nonIndexed( geometry ) { + + return geometry.index ? geometry.toNonIndexed() : geometry; + +} + +// the unit box is identical for every building's shell boxes — build it once +const _unitBox = /*@__PURE__*/ nonIndexed( new BoxGeometry( 1, 1, 1 ) ); + +/** + * Bakes a list of instance groups into one non-indexed BufferGeometry. Each group is a + * base geometry ( position + normal + uv ), an array of Matrix4 placements and a `partId` + * written to a per-vertex attribute. Transforming straight into preallocated typed arrays + * avoids mergeGeometries' per-instance allocations; the result is one geometry, ready for + * a single draw call and the compute rasterizer. + */ +function bakeGroups( groups ) { + + let total = 0; + for ( const group of groups ) total += group.geometry.attributes.position.count * group.matrices.length; + + const position = new Float32Array( total * 3 ); + const normal = new Float32Array( total * 3 ); + const uv = new Float32Array( total * 2 ); + const partId = new Float32Array( total ); + // per-window interior-mapping room ( centre + size ) the glass pane looks into; only + // the glass group writes it, every other vertex stays zero. baked per vertex so the + // material reads each building's own room sizes without a global uniform. + const roomCenter = new Float32Array( total * 3 ); + const roomSize = new Float32Array( total * 2 ); + + let w = 0; + + // the bounding sphere falls out of the AABB gathered while transforming, sparing a + // second full pass over the positions ( computeBoundingSphere ) + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity; + + for ( const group of groups ) { + + const geometry = group.geometry; + const P = geometry.attributes.position.array; + const N = geometry.attributes.normal.array; + const U = geometry.attributes.uv.array; + const count = geometry.attributes.position.count; + const id = group.partId; + const rooms = group.rooms; // per-instance { center, size }, glass only + const rigid = group.rigid === true; // pure rotation ( + translation ): the normal matrix is the rotation itself + + for ( let i = 0; i < group.matrices.length; i ++ ) { + + const room = rooms ? rooms[ i ] : null; + + const matrix = group.matrices[ i ]; + const e = matrix.elements; + const e0 = e[ 0 ], e1 = e[ 1 ], e2 = e[ 2 ], e4 = e[ 4 ], e5 = e[ 5 ], e6 = e[ 6 ], e8 = e[ 8 ], e9 = e[ 9 ], e10 = e[ 10 ], e12 = e[ 12 ], e13 = e[ 13 ], e14 = e[ 14 ]; + + // for a rigid frame the inverse-transpose equals the rotation, so its columns + // are read straight from the matrix and the per-instance 3×3 inverse is skipped + let n0, n1, n2, n3, n4, n5, n6, n7, n8; + + if ( rigid ) { + + n0 = e0; n1 = e1; n2 = e2; n3 = e4; n4 = e5; n5 = e6; n6 = e8; n7 = e9; n8 = e10; + + } else { + + const ne = _normalMatrix.getNormalMatrix( matrix ).elements; + n0 = ne[ 0 ]; n1 = ne[ 1 ]; n2 = ne[ 2 ]; n3 = ne[ 3 ]; n4 = ne[ 4 ]; n5 = ne[ 5 ]; n6 = ne[ 6 ]; n7 = ne[ 7 ]; n8 = ne[ 8 ]; + + } + + for ( let v = 0; v < count; v ++ ) { + + const v3 = v * 3, w3 = w * 3; + const x = P[ v3 ], y = P[ v3 + 1 ], z = P[ v3 + 2 ]; + const wx = e0 * x + e4 * y + e8 * z + e12; + const wy = e1 * x + e5 * y + e9 * z + e13; + const wz = e2 * x + e6 * y + e10 * z + e14; + position[ w3 ] = wx; position[ w3 + 1 ] = wy; position[ w3 + 2 ] = wz; + if ( wx < minX ) minX = wx; if ( wx > maxX ) maxX = wx; + if ( wy < minY ) minY = wy; if ( wy > maxY ) maxY = wy; + if ( wz < minZ ) minZ = wz; if ( wz > maxZ ) maxZ = wz; + + const nx = N[ v3 ], ny = N[ v3 + 1 ], nz = N[ v3 + 2 ]; + const tx = n0 * nx + n3 * ny + n6 * nz, ty = n1 * nx + n4 * ny + n7 * nz, tz = n2 * nx + n5 * ny + n8 * nz; + const inv = 1 / ( Math.sqrt( tx * tx + ty * ty + tz * tz ) || 1 ); + normal[ w3 ] = tx * inv; normal[ w3 + 1 ] = ty * inv; normal[ w3 + 2 ] = tz * inv; + + uv[ w * 2 ] = U[ v * 2 ]; uv[ w * 2 + 1 ] = U[ v * 2 + 1 ]; + partId[ w ] = id; + + if ( room !== null ) { + + roomCenter[ w3 ] = room.center.x; roomCenter[ w3 + 1 ] = room.center.y; roomCenter[ w3 + 2 ] = room.center.z; + roomSize[ w * 2 ] = room.size.x; roomSize[ w * 2 + 1 ] = room.size.y; + + } + + w ++; + + } + + } + + } + + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new BufferAttribute( position, 3 ) ); + geometry.setAttribute( 'normal', new BufferAttribute( normal, 3 ) ); + geometry.setAttribute( 'uv', new BufferAttribute( uv, 2 ) ); + geometry.setAttribute( 'partId', new BufferAttribute( partId, 1 ) ); + geometry.setAttribute( 'roomCenter', new BufferAttribute( roomCenter, 3 ) ); + geometry.setAttribute( 'roomSize', new BufferAttribute( roomSize, 2 ) ); + + geometry.boundingSphere = new Sphere( + new Vector3( ( minX + maxX ) / 2, ( minY + maxY ) / 2, ( minZ + maxZ ) / 2 ), + Math.hypot( maxX - minX, maxY - minY, maxZ - minZ ) / 2 + ); + + return geometry; + +} + +// deterministic PRNG (mulberry32) so a given seed always yields the same tower + +function createRandom( seed ) { + + let s = ( seed >>> 0 ) || 1; + + return function () { + + s = ( s + 0x6D2B79F5 ) | 0; + let t = Math.imul( s ^ ( s >>> 15 ), 1 | s ); + t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t; + return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296; + + }; + +} + +// a stable per-floor hash ( from the floor index and the face origin ) used to pick the +// interior-mapping room module per floor without allocating a closure each floor +function floorHash( f, frame, k ) { + + const s = Math.sin( f * 12.9898 + frame.origin.x * 0.07 + frame.origin.z * 0.131 + k ) * 43758.5453; + return s - Math.floor( s ); + +} + +// the seed-driven "style" of a tower: footprint proportions, tier split and the +// shaping of piers and base arches. these sit between the fixed defaults and the +// caller's parameters, so any parameter passed in still overrides its seeded value. + +function randomStyle( random ) { + + const base = 0.10 + random() * 0.07; + const crown = 0.08 + random() * 0.08; + + return { + footprint: { width: 26 + random() * 18, depth: 20 + random() * 14 }, + tierFractions: { base, crown }, + pierWidth: 0.4 + random() * 0.4, + pierDepth: 0.3 + random() * 0.3, + windowReveal: 0.12 + random() * 0.1, + stringCourseHeight: 0.5 + random() * 0.5, + archBayWidthRatio: Math.round( 1.5 + random() * 1.5 ), + archRise: 0.4 + random() * 0.5 + }; + +} + +/** + * Generates intricate, tripartite "Beaux-Arts / Neo-Gothic" terracotta + * skyscrapers from a small set of parameters. + * + * The mass is read as a footprint polygon (a rectangle with one chamfered + * corner) split into vertical faces, each split into three tiers — a tall + * arcaded base, a repeating shaft and an ornate crown — then into floors and + * bays. A handful of authored pieces (a pier, a window, a cornice profile, a + * gothic arch) are instanced across the whole tower, then baked — together with + * the bespoke base arcade — into a single non-indexed BufferGeometry tagged with + * a per-vertex `partId` ({@link PartId}) so one material can shade every zone. + * + * The generator is material agnostic — it only produces geometry. Pass a single + * material (e.g. a TSL node material that branches on `partId`) to dress it. + * + * ```js + * const generator = new SkyscraperGenerator( { seed: 35, totalHeight: 140 }, material ); + * scene.add( generator.build() ); // a single Mesh + * ``` + */ +class SkyscraperGenerator { + + constructor( parameters = {}, material = null ) { + + this.parameters = parameters; // caller overrides; defaults + seed fill the rest at build time + this.material = material; // a single material; the look is driven by the baked `partId` attribute + + this.mesh = null; + + } + + setParameters( parameters ) { + + Object.assign( this.parameters, parameters ); + + return this; + + } + + build() { + + const random = createRandom( this.parameters.seed ?? SkyscraperGenerator.defaults.seed ); + + // precedence: fixed defaults < seed-driven style < caller parameters + + const p = Object.assign( {}, SkyscraperGenerator.defaults, randomStyle( random ), this.parameters ); + + // snap the masonry-driving dimensions to the brick module so the procedural + // brickwork ( courses up local Y, columns along each face ) lines up with the + // geometry: a whole number of courses per floor and bricks per bay + const vModule = BRICK.height * 2; // a course pair, so floor / window halves still land on a joint + p.floorHeight = Math.max( vModule * 3, Math.round( p.floorHeight / vModule ) * vModule ); + p.windowHeight = Math.round( p.floorHeight * WINDOW_HEIGHT_RATIO / vModule ) * vModule; + p.bayWidth = Math.max( BRICK.length * 3, Math.round( p.bayWidth / BRICK.length ) * BRICK.length ); + p.pierWidth = Math.max( BRICK.length, Math.round( p.pierWidth / BRICK.length ) * BRICK.length ); + + // vertical layout: base / shaft / crown as whole floor counts, so every floor + // line sits on a course ( the requested total height is rounded to suit ) + const floors = Math.max( 3, Math.round( p.totalHeight / p.floorHeight ) ); + const baseFloors = Math.max( 1, Math.round( floors * p.tierFractions.base ) ); + const crownFloors = Math.max( 1, Math.round( floors * p.tierFractions.crown ) ); + const shaftFloors = Math.max( 1, floors - baseFloors - crownFloors ); + + const baseHeight = baseFloors * p.floorHeight; + const crownHeight = crownFloors * p.floorHeight; + const shaftHeight = shaftFloors * p.floorHeight; + p.totalHeight = baseHeight + shaftHeight + crownHeight; + + const baseTop = baseHeight; + const shaftTop = baseHeight + shaftHeight; + + // one accumulator per kind of part, mostly instance matrices. kept separate so the + // bake below can order them by draw order ( which controls overdraw ), not build order. + + const windows = []; + const glass = []; + const glassRooms = []; // per-glass interior-mapping room ( centre + size ), aligned with `glass` + const backWalls = []; // the thin wall closing the volume behind the glass + const bands = []; // spandrel bands, one at each floor line + const piers = new Map(); // pier height -> matrices, so each tier's continuous piers share one geometry + const trim = []; // cornices and parapets ( axis-aligned unit boxes ) + const acUnits = []; // window air-conditioner boxes on a random subset of shaft windows + const finials = []; // pinnacles along the crown + const extras = []; // bespoke geometry: the base arcade and the setback / roof slabs + + const addPier = ( frame, u, vBottom, height ) => { + + const key = Math.round( height * 1000 ); // bucket equal pier heights ( a number key, no string ) + if ( piers.has( key ) === false ) piers.set( key, [] ); + piers.get( key ).push( frame.matrix( u, vBottom, 0 ) ); + + }; + + // footprints: full mass, and the inset crown after the setback + + const footprint = buildFootprint( p.footprint.width, p.footprint.depth, p.chamferWidth, p.chamferCornerX, p.chamferCornerZ ); + const faces = buildFaces( footprint ); + + const inset = p.setbackDepth * p.bayWidth; + const crownFootprint = buildFootprint( + Math.max( p.bayWidth * 2, p.footprint.width - inset * 2 ), + Math.max( p.bayWidth * 2, p.footprint.depth - inset * 2 ), + Math.max( 0, p.chamferWidth - inset ), + p.chamferCornerX, + p.chamferCornerZ + ); + const crownFaces = buildFaces( crownFootprint ); + + // --- generate the parts ----------------------------------------------- + + const crownCornice = p.stringCourseHeight * 1.6; // the crown's heavy cap; its piers stop below it + + // shaft and crown are the same facade over different faces, spans and pier heights + const tiers = [ + { faces, bottom: baseTop, height: shaftHeight, pierHeight: shaftHeight, ac: acUnits }, + { faces: crownFaces, bottom: shaftTop, height: crownHeight, pierHeight: crownHeight - crownCornice, ac: null } + ]; + + for ( const t of tiers ) { + + for ( const frame of t.faces ) { + + addWindows( frame, windows, glass, glassRooms, t.ac, t.bottom, t.height, p ); + addWall( backWalls, frame, t.bottom, t.bottom + t.height, 0.8, - 0.6 ); + addSpandrelBands( bands, frame, t.bottom, t.height, p ); + addPiers( frame, t.bottom, t.pierHeight, p, addPier ); + + } + + } + + // the base: a gothic arcade, capped by a string course + for ( const frame of faces ) { + + addArcade( extras, frame, baseHeight, p ); + addCornice( trim, frame, baseTop - p.stringCourseHeight, p.stringCourseHeight, 0.5 ); + + } + + // periodic string courses banding the shaft + if ( p.stringCourseEvery > 0 ) { + + for ( let f = p.stringCourseEvery; f < shaftFloors; f += p.stringCourseEvery ) { + + for ( const frame of faces ) addCornice( trim, frame, baseTop + f * p.floorHeight - p.stringCourseHeight * 0.5, p.stringCourseHeight, 0.3 ); + + } + + } + + // the crown's heavy cornice, its parapet and the finials along the top + for ( const frame of crownFaces ) { + + addCornice( trim, frame, p.totalHeight - crownCornice, crownCornice, 0.9 ); + addParapet( trim, frame, p.totalHeight, p ); + addFinials( frame, finials, shaftTop, crownHeight, p ); + + } + + // thin slabs capping the setback ledge and the roof + extras.push( slab( footprint, shaftTop, 0.6 ) ); + extras.push( slab( crownFootprint, p.totalHeight, 0.6 ) ); + + // --- bake every part into one geometry --------------------------------- + + // one mesh = one draw the renderer can't sort, so bake order is draw order: the + // facade front-to-back, the backing wall last so its hidden fragments never shade. + + const groups = [ + { geometry: buildWindowGeometry( p ), matrices: windows, partId: FRAME, rigid: true }, + { geometry: nonIndexed( buildGlassGeometry( p ) ), matrices: glass, partId: GLASS, rooms: glassRooms, rigid: true }, + { geometry: _unitBox, matrices: bands, partId: WALL } + ]; + + for ( const [ key, matrices ] of piers ) groups.push( { geometry: buildPierGeometry( p, key / 1000 ), matrices, partId: PIER, rigid: true } ); + + groups.push( { geometry: _unitBox, matrices: trim, partId: WALL } ); // cornices, parapets + groups.push( { geometry: _unitBox, matrices: acUnits, partId: AC } ); + groups.push( { geometry: nonIndexed( buildFinialGeometry( p ) ), matrices: finials, partId: ORNAMENT, rigid: true } ); + + for ( const geometry of extras ) groups.push( { geometry: nonIndexed( geometry ), matrices: [ _identity ], partId: WALL, rigid: true } ); // base arcade + slabs, in building-local space + + groups.push( { geometry: _unitBox, matrices: backWalls, partId: WALL } ); // last — hidden behind the facade + + const geometry = bakeGroups( groups ); + + const mesh = new Mesh( geometry, this.material || new MeshStandardMaterial( { color: 0xddccaa, roughness: 0.9 } ) ); + mesh.name = 'Skyscraper'; + + this.dispose(); + this.mesh = mesh; + + return mesh; + + } + + rebuild() { + + return this.build(); + + } + + dispose() { + + if ( this.mesh === null ) return; + + this.mesh.geometry.dispose(); + this.mesh = null; + + } + +} + +// fixed baseline. the remaining parameters (footprint, tierFractions, pierWidth, +// pierDepth, windowReveal, stringCourseHeight, archBayWidthRatio, archRise) are +// derived from the seed by randomStyle() unless the caller provides them. +SkyscraperGenerator.defaults = { + seed: 35, + totalHeight: 140, + floorHeight: 4, + bayWidth: 2.6, + stringCourseEvery: 6, + chamferWidth: 4, + chamferCornerX: 1, + chamferCornerZ: 1, + setbackDepth: 1.5, + acChance: 0.12 +}; + +// --- footprint & faces --------------------------------------------------- + +/** + * A rectangle (centred at the origin in the XZ plane) with one corner cut at + * 45 degrees, returned as an ordered list of `Vector2( x, z )`. `cornerX` / + * `cornerZ` ( each ±1 ) pick which corner is cut, so the chamfer can be aimed + * outward to a block corner. + */ +function buildFootprint( width, depth, chamfer, cornerX = 1, cornerZ = 1 ) { + + const hw = width / 2; + const hd = depth / 2; + const c = Math.min( chamfer, hw, hd ); + + // the four corners, counter-clockwise + const corners = [ + new Vector2( hw, hd ), + new Vector2( - hw, hd ), + new Vector2( - hw, - hd ), + new Vector2( hw, - hd ) + ]; + + const points = []; + + for ( let i = 0; i < corners.length; i ++ ) { + + const corner = corners[ i ]; + + // cut the requested corner: replace it with two points pulled back along + // each adjacent edge, leaving a 45° face that points out to that corner + if ( c > 0 && Math.sign( corner.x ) === cornerX && Math.sign( corner.y ) === cornerZ ) { + + const prev = corners[ ( i + 3 ) % 4 ]; + const next = corners[ ( i + 1 ) % 4 ]; + points.push( corner.clone().lerp( prev, c / corner.distanceTo( prev ) ) ); + points.push( corner.clone().lerp( next, c / corner.distanceTo( next ) ) ); + + } else { + + points.push( corner.clone() ); + + } + + } + + return points; + +} + +/** + * Builds a face frame per footprint edge. Each frame is an orthonormal basis + * ( u along the edge, v up, n outward ) plus an origin and length, so all + * facade layout can happen in flat ( u, v ) space and bake to world with one + * matrix — the same authored piece then instances onto every face, including + * the diagonal chamfer. + */ +function buildFaces( points ) { + + const faces = []; + const up = new Vector3( 0, 1, 0 ); + + for ( let i = 0; i < points.length; i ++ ) { + + const a = points[ i ]; + const b = points[ ( i + 1 ) % points.length ]; + + // outward normal: perpendicular to the edge, pointing away from the + // origin (the footprint is centred there) + + const n = new Vector3( b.y - a.y, 0, - ( b.x - a.x ) ).normalize(); + const mid = new Vector3( ( a.x + b.x ) / 2, 0, ( a.y + b.y ) / 2 ); + if ( n.dot( mid ) < 0 ) n.negate(); + + // right-handed basis: u = v × n, so makeBasis( u, v, n ) is a pure rotation + + const u = new Vector3().crossVectors( up, n ).normalize(); + + const pa = new Vector3( a.x, 0, a.y ); + const pb = new Vector3( b.x, 0, b.y ); + const length = pa.distanceTo( pb ); + + // the edge end that u points away from becomes the origin + + const origin = pb.clone().sub( pa ).dot( u ) > 0 ? pa : pb; + + faces.push( new FaceFrame( origin, u, up.clone(), n, length ) ); + + } + + return faces; + +} + +/** A face's local ( u along edge, v up, n outward ) frame in world space. */ +class FaceFrame { + + constructor( origin, u, v, n, length ) { + + this.origin = origin; + this.u = u; + this.v = v; + this.n = n; + this.length = length; + + } + + point( u, v, w, target = new Vector3() ) { + + return target + .copy( this.origin ) + .addScaledVector( this.u, u ) + .addScaledVector( this.v, v ) + .addScaledVector( this.n, w ); + + } + + /** Places a piece authored in the canonical local frame ( x across, y up, z outward ). */ + matrix( u, v, w ) { + + return new Matrix4() + .makeBasis( this.u, this.v, this.n ) + .setPosition( this.point( u, v, w, _point ) ); + + } + + /** How many bays of `bayWidth` fit, with the remainder split into end margins. */ + bays( bayWidth ) { + + const count = Math.max( 1, Math.floor( this.length / bayWidth ) ); + const margin = ( this.length - count * bayWidth ) / 2; + + return { count, margin, width: bayWidth }; + + } + +} + +// --- shell pieces -------------------------------------------------------- + +// a Matrix4 mapping the shared unit box ( 1×1×1, centred ) onto a face-aligned +// box of the given size, centred at the given face-local point. these matrices +// are what the shell InstancedMesh is built from. +function boxMatrix( frame, u, v, w, sizeU, sizeV, sizeN ) { + + return new Matrix4() + .makeBasis( frame.u, frame.v, frame.n ) + .scale( _scale.set( sizeU, sizeV, sizeN ) ) + .setPosition( frame.point( u, v, w, _point ) ); + +} + +function addWall( target, frame, vBottom, vTop, thickness = 0.8, front = 0 ) { + + const h = vTop - vBottom; + target.push( boxMatrix( frame, frame.length / 2, vBottom + h / 2, front - thickness / 2, frame.length + thickness * 2, h, thickness ) ); + +} + +/** + * Horizontal terracotta bands at every floor line. Together with the projecting + * piers they form the facade grid; the gaps between them are the window + * openings, with glass set behind. + */ +function addSpandrelBands( target, frame, vBottom, height, p ) { + + const floors = Math.max( 1, Math.round( height / p.floorHeight ) ); + const fh = height / floors; + const bandHeight = p.floorHeight - p.windowHeight; // whole courses: floor minus the glazed opening + + // pull the ends in by the band depth so a band doesn't poke its end-cap + // into the plane of the perpendicular face at the corners ( overdraw ) + const bandLength = Math.max( 0.2, frame.length - 0.6 ); + + for ( let f = 0; f <= floors; f ++ ) { + + // front flush at w = 0, meeting the backing wall behind + target.push( boxMatrix( frame, frame.length / 2, vBottom + f * fh, - 0.3, bandLength, bandHeight, 0.6 ) ); + + } + +} + +/** + * A thin horizontal cap over a footprint's bounding box at height `y`. Its + * sides are pulled in behind the facade plane ( into the backing-wall shell ) + * so they never sit coplanar with the walls, spandrels or piers and z-fight. + */ +function slab( footprint, y, thickness ) { + + // a thin cap following the footprint OUTLINE ( so the chamfered corner is cut, not + // left overhanging as a rectangular box ), inset a little so its edge tucks just + // behind the facade and the wall top reads as a lip around it + + const inset = 0.8; + let cx = 0, cz = 0; + for ( const p of footprint ) { + + cx += p.x; cz += p.y; + + } + + cx /= footprint.length; cz /= footprint.length; + + // consistent ( CCW ) winding so the extrude caps face up / down correctly + let area = 0; + for ( let i = 0; i < footprint.length; i ++ ) { + + const a = footprint[ i ], b = footprint[ ( i + 1 ) % footprint.length ]; + area += a.x * b.y - b.x * a.y; + + } + + const pts = area < 0 ? footprint.slice().reverse() : footprint; + + const shape = new Shape(); + pts.forEach( ( p, i ) => { + + const dx = cx - p.x, dz = cz - p.y; + const d = Math.hypot( dx, dz ) || 1; + const x = p.x + dx / d * inset; + const z = p.y + dz / d * inset; + if ( i === 0 ) shape.moveTo( x, z ); else shape.lineTo( x, z ); + + } ); + + // extrude the XZ outline downward by the thickness, the top dropped just below height y: + // the inset cap would otherwise sit coplanar with the surrounding wall top faces and + // z-fight, and the parapet / spandrel bands around the edge hide the shallow recess + const drop = 0.2; + const geometry = new ExtrudeGeometry( shape, { depth: thickness, bevelEnabled: false } ); + geometry.rotateX( Math.PI / 2 ); + geometry.translate( 0, y - drop, 0 ); + return geometry; + +} + +/** A two-step projecting cornice / string-course band wrapping a face. */ +function addCornice( target, frame, vBottom, height, depth ) { + + target.push( boxMatrix( frame, frame.length / 2, vBottom + height * 0.275, depth / 2, frame.length, height * 0.55, depth ) ); + target.push( boxMatrix( frame, frame.length / 2, vBottom + height * 0.775, depth * 0.85, frame.length, height * 0.45, depth * 1.7 ) ); + +} + +/** A low parapet wall capping the crown. */ +function addParapet( target, frame, vTop, p ) { + + const height = 1.4; + target.push( boxMatrix( frame, frame.length / 2, vTop + height / 2, p.pierDepth * 0.4, frame.length, height, p.pierDepth * 0.8 ) ); + +} + +/** + * The base storey: a wall pierced by tall pointed-arch openings, extruded with + * thickness so the openings read as deep recesses. + */ +function addArcade( target, frame, height, p ) { + + const archWidth = p.bayWidth * p.archBayWidthRatio; + const { count, margin } = frame.bays( archWidth ); + + const sill = height * 0.04; + const spring = height * 0.55; + const apex = Math.min( height * 0.96, spring + ( archWidth / 2 ) * ( 0.8 + p.archRise ) ); + + const shape = new Shape(); + shape.moveTo( 0, 0 ); + shape.lineTo( frame.length, 0 ); + shape.lineTo( frame.length, height ); + shape.lineTo( 0, height ); + shape.lineTo( 0, 0 ); + + for ( let i = 0; i < count; i ++ ) { + + const cx = margin + ( i + 0.5 ) * archWidth; + const hw = archWidth * 0.34; + + const hole = new Path(); + hole.moveTo( cx - hw, sill ); + hole.lineTo( cx - hw, spring ); + hole.quadraticCurveTo( cx - hw, apex, cx, apex ); + hole.quadraticCurveTo( cx + hw, apex, cx + hw, spring ); + hole.lineTo( cx + hw, sill ); + hole.lineTo( cx - hw, sill ); + shape.holes.push( hole ); + + } + + const thickness = 1.1; + const geometry = new ExtrudeGeometry( shape, { depth: thickness, bevelEnabled: false, curveSegments: 8 } ); + geometry.translate( 0, 0, - thickness ); + geometry.applyMatrix4( frame.matrix( 0, 0, 0 ) ); + + target.push( geometry ); + + // a dark plane set behind the openings so the recesses read + + const back = new PlaneGeometry( frame.length, height ); + back.applyMatrix4( frame.matrix( frame.length / 2, height / 2, - thickness - 0.4 ) ); + target.push( back ); + +} + +// --- repeating field ----------------------------------------------------- + +function addPiers( frame, vBottom, height, p, addPier ) { + + const { count, margin, width } = frame.bays( p.bayWidth ); + + // a pier on every bay edge except the far end: that corner is shared with + // the next face, which places its own pier there, so emitting both would + // stack two piers at each corner + + for ( let i = 0; i < count; i ++ ) { + + addPier( frame, margin + i * width, vBottom, height ); + + } + +} + +function addWindows( frame, windows, glass, glassRooms, acUnits, vBottom, height, p ) { + + const { count, margin, width } = frame.bays( p.bayWidth ); + const floors = Math.max( 1, Math.round( height / p.floorHeight ) ); + const fh = height / floors; + + // a window AC unit sitting on the sill, protruding from the facade. about half the window + // width, capped at a real unit's size ( ~0.66 m ) and kept wider than tall, sticking out + // about half its width + const acW = Math.min( ( p.bayWidth - p.pierWidth ) * 0.55, 0.66 ); + const acH = acW * 0.6; + const acD = acW * 0.5; + const acV = - p.windowHeight / 2 + acH / 2 + WINDOW_BORDER; // bottom rests on the sill ( the top of the window's bottom frame rail ) + + // a real ~0.66 m unit looks lost in a wide opening, so only fit ACs where it still spans a + // fair share of the window — in practice, the narrower ( older-style ) windows + const acFits = acW >= ( width - p.pierWidth ) * 0.34; + + for ( let f = 0; f < floors; f ++ ) { + + const cy = vBottom + ( f + 0.5 ) * fh; + + // the interior-mapping room module: one floor tall, a run of two or three bays + // wide, chosen per floor so neighbouring windows share an interior. the choice + // is deterministic ( seeded by the floor and the face ) so it is stable, and the + // run is recorded per window so the material can ray-march the right box. + const roomBays = floorHash( f, frame, 0 ) > 0.5 ? 3 : 2; + const roomPhase = Math.floor( floorHash( f, frame, 1 ) * roomBays ); + + for ( let b = 0; b < count; b ++ ) { + + const cx = margin + ( b + 0.5 ) * width; + + windows.push( frame.matrix( cx, cy, 0 ) ); + glass.push( frame.matrix( cx, cy, - p.windowReveal ) ); + + // the run of bays this window's room spans, clamped at the face ends, recorded + // as the room's centre on the facade and its width × height in metres + const room = Math.floor( ( b + roomPhase ) / roomBays ); + const bStart = Math.max( 0, room * roomBays - roomPhase ); + const bEnd = Math.min( count, ( room + 1 ) * roomBays - roomPhase ); + const span = bEnd - bStart; + glassRooms.push( { center: frame.point( margin + ( bStart + span / 2 ) * width, cy, - p.windowReveal ), size: new Vector2( span * width, fh - 1 ) } ); // centred on the glass plane, so the interior is anchored to the pane it is drawn on + + if ( acUnits && acFits ) { + + // deterministic per-window hash ( varies per face via the frame origin ) + const r = Math.sin( f * 41.3 + b * 12.7 + frame.origin.x * 0.13 + frame.origin.z * 0.31 ) * 43758.5453; + // the back tucks into the window reveal ( just in front of the glass ) so the unit sits + // in the opening instead of floating on the facade + const acW0 = acD / 2 - p.windowReveal + 0.04; + if ( r - Math.floor( r ) < p.acChance ) acUnits.push( boxMatrix( frame, cx, cy + acV, acW0, acW, acH, acD ) ); + + } + + } + + } + +} + +function addFinials( frame, finials, vBottom, height, p ) { + + const { count, margin, width } = frame.bays( p.bayWidth ); + const top = vBottom + height; + + // skip the far-end bay edge: it is the shared corner the next face also + // caps, so emitting both would stack two finials at each corner + + for ( let i = 0; i < count; i ++ ) { + + finials.push( new Matrix4().setPosition( frame.point( margin + i * width, top, p.pierDepth * 0.5, _point ) ) ); + + } + +} + +// --- authored modules ---------------------------------------------------- + +function buildPierGeometry( p, height ) { + + // a wide pier with a slimmer pilaster raised on its face, giving the + // continuous vertical rib a stepped, terracotta profile + + const back = new BoxGeometry( p.pierWidth, height, p.pierDepth * 0.6 ); + back.translate( 0, height / 2, p.pierDepth * 0.3 ); + + // the pilaster stops just short of the pier top so that where a pier is left + // exposed ( at a setback ) the cap reads as one clean block rather than the + // back box and the pilaster stacked into a T + const pilasterHeight = Math.max( 1, height - 0.6 ); + const front = new BoxGeometry( p.pierWidth * 0.55, pilasterHeight, p.pierDepth * 0.45 ); + front.translate( 0, pilasterHeight / 2, p.pierDepth * 0.6 + p.pierDepth * 0.225 ); + + return merge( [ back, front ] ); + +} + +function buildWindowGeometry( p ) { + + // the flat frame face ( a rectangle with the glazing hole ), the four reveal walls + // of the opening and the glazing bars, merged into one instanced module. a full + // extrusion would also emit a hidden back cap and outer side walls; windows are by + // far the heaviest part of a building, so those are skipped. + + const w = p.bayWidth - p.pierWidth; + const h = p.windowHeight; + const border = WINDOW_BORDER; + const depth = p.windowReveal; // reveal walls run all the way back to the glass ( placed at -windowReveal ), so no gap opens between them and the pane + const iw = w / 2 - border; + const ih = h / 2 - border; + + const shape = new Shape(); + shape.moveTo( - w / 2, - h / 2 ); + shape.lineTo( w / 2, - h / 2 ); + shape.lineTo( w / 2, h / 2 ); + shape.lineTo( - w / 2, h / 2 ); + shape.lineTo( - w / 2, - h / 2 ); + + const hole = new Path(); + hole.moveTo( - iw, - ih ); + hole.lineTo( - iw, ih ); + hole.lineTo( iw, ih ); + hole.lineTo( iw, - ih ); + hole.lineTo( - iw, - ih ); + shape.holes.push( hole ); + + const front = new ShapeGeometry( shape ); // visible frame face, flush with the facade + + // the four reveal walls of the opening, set back to the glazing + const wall = ( x, y, rx, ry, sw, sh ) => { + + const pl = new PlaneGeometry( sw, sh ); + pl.rotateX( rx ); + pl.rotateY( ry ); + pl.translate( x, y, - depth / 2 ); + return pl; + + }; + + const left = wall( - iw, 0, 0, Math.PI / 2, depth, ih * 2 ); + const right = wall( iw, 0, 0, - Math.PI / 2, depth, ih * 2 ); + const sill = wall( 0, - ih, - Math.PI / 2, 0, iw * 2, depth ); + const head = wall( 0, ih, Math.PI / 2, 0, iw * 2, depth ); + + // a single horizontal glazing bar ( transom ), flat, just in front of the glass — + // a thin box would triple the window's triangle count for sub-pixel thickness + const transom = new PlaneGeometry( iw * 2, 0.05 ); + transom.translate( 0, h * 0.04, - depth + 0.02 ); // meeting rail, just above centre + + return merge( [ front, left, right, sill, head, transom ] ); + +} + +function buildGlassGeometry( p ) { + + const w = p.bayWidth - p.pierWidth - WINDOW_BORDER * 2; + const h = p.windowHeight - WINDOW_BORDER * 2; + + return new PlaneGeometry( w, h ); + +} + +function buildFinialGeometry( p ) { + + // a tapering pinnacle revolved around its axis + + const s = p.pierWidth; + const profile = [ + new Vector2( 0.0, 0 ), + new Vector2( s * 0.9, 0 ), + new Vector2( s * 0.9, s * 0.4 ), + new Vector2( s * 0.55, s * 1.0 ), + new Vector2( 0.0, s * 3.2 ) + ]; + + return new LatheGeometry( profile, 8 ); // round enough to read as a smooth pinnacle, still light + +} + +// --- material ------------------------------------------------------------ + +// derivative-based bump for a procedural, world-space height field. the built-in bumpMap +// offsets the UV to read its height, so it returns a zero gradient for a height keyed off +// world position; this feeds the hardware screen-space derivatives of the height into +// Mikkelsen's surface-gradient method so the relief actually perturbs the normal. +function bumpNormal( height ) { + + const dpdx = positionView.dFdx(); + const dpdy = positionView.dFdy(); + const r1 = dpdy.cross( normalView ); + const r2 = normalView.cross( dpdx ); + const det = dpdx.dot( r1 ); + const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) ); + + return det.abs().mul( normalView ).sub( grad ).normalize(); + +} + +// interior mapping: fakes a furnished room behind each glass pane in the fragment +// shader — no geometry, no texture. every pane carries the room it looks into ( centre + +// size, baked per window by addWindows ), so neighbouring panes share one interior. the +// view ray is cast into that box and the walls, floor, ceiling and a few furniture pieces +// it meets are shaded procedurally, keyed off a per-room hash. returns vec4( colour, lit ). +const interior = /*@__PURE__*/ Fn( () => { + + // flat so floor() below can't split one pane across two cell ids ( centre is per-room ) + const roomCenter = varying( attribute( 'roomCenter', 'vec3' ) ).setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER ); + const roomSize = attribute( 'roomSize', 'vec2' ); + + // a per-face frame from the geometry normal ( holds on every facade, including the + // 45° chamfer ): u runs across the face, v is up, n points outward + const n = normalLocal; + const up = vec3( 0, 1, 0 ); + const uAxis = cross( up, n ).normalize(); + + // this pixel and the view ray, in the room's ( across, up, depth ) frame; depth + // runs into the wall, so the ray's depth component is positive + const d = positionLocal.sub( roomCenter ); + const camLocal = modelWorldMatrixInverse.mul( vec4( cameraPosition, 1 ) ).xyz; + const rayLocal = positionLocal.sub( camLocal ).normalize(); + const origin = vec3( dot( d, uAxis ), d.y, 0 ); + const dir = vec3( dot( rayLocal, uAxis ), rayLocal.y, dot( rayLocal, n ).negate() ); + + // the room box: the pane-wide × ceiling-height front rectangle ( centred on the pane ), + // set back behind the glass and run a little deeper than it is tall. shade the far + // side the ray exits ( slab method: nearest of the three far-plane crossings; + // dividing by a near-zero direction gives ±inf, which min() harmlessly drops ). + const setback = float( 0.1 ); // the room starts just behind the glass, so it sits flush in the frame opening + const boxMax = vec3( roomSize.x.mul( 0.5 ), roomSize.y.mul( 0.5 ), setback.add( roomSize.y.mul( 1.55 ) ) ); + const boxMin = vec3( boxMax.x.negate(), boxMax.y.negate(), setback ); + const tFar = boxMin.sub( origin ).div( dir ).max( boxMax.sub( origin ).div( dir ) ); + const t = tFar.x.min( tFar.y ).min( tFar.z ); + const hit = origin.add( dir.mul( t ) ); + const q = hit.sub( boxMin ).div( boxMax.sub( boxMin ) ); // 0..1 inside the room + + const onBack = q.z.greaterThan( 0.998 ); + const onCeil = q.y.greaterThan( 0.998 ); + const onFloor = q.y.lessThan( 0.002 ); + + // per-room key for a portable integer hash — fract( sin() ) isn't bit-exact across drivers + const cell = floor( roomCenter.mul( 2.0 ) ); // + offset before the u32 cast keeps it non-negative + const ckey = uint( cell.x.add( 1 << 21 ) ).mul( uint( 73856093 ) ) + .bitXor( uint( cell.y.add( 1 << 21 ) ).mul( uint( 19349663 ) ) ) + .bitXor( uint( cell.z.add( 1 << 21 ) ).mul( uint( 83492791 ) ) ).toVar(); + const hash = ( kx, ky, kz ) => ihash( ckey.add( uint( Math.round( ( kx + ky * 7 + kz * 13 ) * 100 ) ) ) ); + const seed = hash( 12.9898, 78.233, 37.719 ); + const seed2 = hash( 39.346, 11.135, 83.155 ); + const lit = step( 0.8, hash( 63.21, 9.17, 51.43 ) ); // ~20% of rooms have the lights on; the rest sit dark + + // each room's bulb colour. most run warm, drifting from a dim amber ( ~2400K ) up to a + // warm white ( ~3200K ); a minority run cool, from a fluorescent / LED daylight to a TV's + // bluer glow — so a lit facade reads as a spread of bulb temperatures, not one flat tint + const warmLight = mix( color( 0xffb845 ), color( 0xffe49c ), hash( 27.1, 4.9, 61.7 ) ); + const coolLight = mix( color( 0xdfe8ff ), color( 0x9fb6ff ), hash( 8.3, 51.2, 17.6 ) ); + const lightCol = select( hash( 44.7, 19.3, 6.1 ).greaterThan( 0.88 ), coolLight, warmLight ); // ~12% of lit rooms run cool + + // depth falloff ( darker toward the back ), and a panel mask on a face given its + // two 0..1 coordinates — used for the flat fittings below + const depth = roomSize.y.mul( 1.55 ); + const falloffAt = ( z ) => mix( float( 1.0 ), float( 0.42 ), z.sub( setback ).div( depth ).clamp( 0, 1 ) ); + const rect = ( ax, ay, cx, cy, hw, hh ) => smoothstep( hw + 0.006, hw - 0.006, ax.sub( cx ).abs() ).mul( smoothstep( hh + 0.006, hh - 0.006, ay.sub( cy ).abs() ) ); + + // --- the room shell: walls, floor, ceiling, back wall, with flat fittings ---- + + // muted plaster, picked per room, with a darker skirting board along the wall foot + let wall = mix( color( 0x9a8b73 ), color( 0x6f7a82 ), seed ); + wall = mix( wall, color( 0xb9ad97 ), seed2.mul( 0.6 ) ); + const wallCol = mix( wall, wall.mul( 0.5 ), smoothstep( 0.05, 0.04, q.y ) ); + + // floorboards with a thin seam every few, and a centred rug + const seam = step( 0.94, fract( q.x.mul( 6 ) ) ); + const boards = mix( color( 0x4a3320 ), color( 0x6a4c30 ), seed ).mul( seam.mul( 0.3 ).oneMinus() ); + const rug = mix( color( 0x7a3b32 ), color( 0x3a5760 ), seed2 ); + const floorCol = mix( boards, rug, rect( q.x, q.z, 0.5, 0.62, 0.3, 0.26 ).mul( 0.9 ) ); + + // ceiling, lighter than the walls, with a round overhead light in the middle; in a + // lit room the fixture reads bright and glows ( the material's emissive = colour × lit ) + const lamp = smoothstep( 0.16, 0.13, vec2( q.x.sub( 0.5 ), q.z.sub( 0.5 ) ).length() ); + const ceilCol = mix( mix( wall, color( 0xffffff ), 0.5 ), lightCol.mul( mix( float( 1.0 ), float( 4.5 ), lit ) ), lamp ); + + // back wall: a panelled door to one side, and a framed picture kept on the + // opposite half of the wall so it never lands on the door + const doorX = mix( float( 0.22 ), float( 0.78 ), seed ); + const door = mix( color( 0x5a4631 ), color( 0x39383c ), step( 0.5, seed2 ) ); + const picX = select( doorX.lessThan( 0.5 ), mix( float( 0.68 ), float( 0.82 ), seed2 ), mix( float( 0.18 ), float( 0.32 ), seed2 ) ); + const picCol = mix( color( 0x2c3a4a ), color( 0x7a5a3a ), hash( 5.1, 9.2, 3.3 ) ); + let backCol = mix( wallCol, door, rect( q.x, q.y, doorX, 0.33, 0.085, 0.35 ) ); + backCol = mix( backCol, color( 0x141210 ), rect( q.x, q.y, picX, 0.56, 0.075, 0.085 ) ); // dark frame + backCol = mix( backCol, picCol, rect( q.x, q.y, picX, 0.56, 0.055, 0.065 ) ); // the picture + + const shellCol = select( onBack, backCol, select( onCeil, ceilCol, select( onFloor, floorCol, wallCol ) ) ); + + // fake ambient occlusion: darken the hit toward the room's edges ( where two surfaces + // meet ), so the box reads with soft corner shading instead of flat-lit walls. the two + // in-plane axes depend on which face the ray exits through ( q is 0..1 inside the room ). + const aoBand = 0.15; + const aoEdge = ( a ) => smoothstep( 0, aoBand, a ).mul( smoothstep( 0, aoBand, a.oneMinus() ) ); + const edgeAO = select( onBack, aoEdge( q.x ).mul( aoEdge( q.y ) ), select( onFloor.or( onCeil ), aoEdge( q.x ).mul( aoEdge( q.z ) ), aoEdge( q.y ).mul( aoEdge( q.z ) ) ) ); + const shellAO = mix( float( 0.72 ), float( 1.0 ), edgeAO ); + + // --- nearest surface: the shell, then any furniture block that lies closer ---- + // each block is a solid axis-aligned box in room space; boxHit returns its near + // face. consider() keeps whichever surface the ray meets first. + let bestT = t; + let bestCol = shellCol.mul( shellAO ).mul( falloffAt( hit.z ) ); + let bestEmit = float( 1 ); // per-hit emissive weight: shell and fittings emit fully, curtains far less + + const boxHit = ( bMin, bMax ) => { + + const ta = bMin.sub( origin ).div( dir ); + const tb = bMax.sub( origin ).div( dir ); + const lo = ta.min( tb ), hi = ta.max( tb ); + const tN = lo.x.max( lo.y ).max( lo.z ); + const p = origin.add( dir.mul( tN ) ); + return { tN, p, hit: hi.x.min( hi.y ).min( hi.z ).greaterThan( tN ).and( tN.greaterThan( 0 ) ), qb: p.sub( bMin ).div( bMax.sub( bMin ) ) }; + + }; + + const consider = ( h, tN, c, emit = 1 ) => { + + const near = h.and( tN.lessThan( bestT ) ); bestCol = select( near, c, bestCol ); bestEmit = select( near, float( emit ), bestEmit ); bestT = select( near, tN, bestT ); + + }; + + const halfU = boxMax.x, floorY = boxMin.y, ceilY = boxMax.y, backZ = boxMax.z; + const midZ = setback.add( depth.mul( 0.5 ) ); // room centre, in depth + + // a low table near the middle of the room ( its top catches the light ) + const tCx = mix( float( - 0.6 ), float( 0.6 ), seed ); + const tCz = midZ.add( mix( float( - 0.4 ), float( 0.5 ), seed2 ) ); + const tbl = boxHit( vec3( tCx.sub( 0.6 ), floorY, tCz.sub( 0.35 ) ), vec3( tCx.add( 0.6 ), floorY.add( 0.42 ), tCz.add( 0.35 ) ) ); + const tblCol = mix( color( 0x4a3526 ), color( 0x6b4a30 ), seed2 ).mul( select( tbl.qb.y.greaterThan( 0.94 ), float( 1.25 ), float( 0.8 ) ) ); + consider( tbl.hit, tbl.tN, tblCol.mul( falloffAt( tbl.p.z ) ) ); + + // a wide low sofa against the back wall, facing the window + const sofaCx = mix( halfU.mul( - 0.3 ), halfU.mul( 0.3 ), seed2 ); + const sofa = boxHit( vec3( sofaCx.sub( 1.1 ), floorY, backZ.sub( 0.95 ) ), vec3( sofaCx.add( 1.1 ), floorY.add( mix( float( 0.8 ), float( 0.9 ), seed ) ), backZ.sub( 0.1 ) ) ); + const sofaCol = mix( color( 0x5a4a3a ), color( 0x42566a ), seed ).mul( select( sofa.qb.y.greaterThan( 0.9 ), float( 1.12 ), float( 0.85 ) ) ); + consider( sofa.hit, sofa.tN, sofaCol.mul( falloffAt( sofa.p.z ) ) ); + + // tall wardrobes in the back corners — each side stands in some rooms + const wardrobe = ( cx, gate, h ) => { + + const w = boxHit( vec3( cx.sub( 0.5 ), floorY, backZ.sub( 0.7 ) ), vec3( cx.add( 0.5 ), floorY.add( h ), backZ.sub( 0.1 ) ) ); + const c = mix( color( 0x3a2c22 ), color( 0x55473a ), seed ).mul( select( w.qb.y.greaterThan( 0.94 ), float( 1.2 ), float( 0.82 ) ) ); + consider( w.hit.and( gate ), w.tN, c.mul( falloffAt( w.p.z ) ) ); + + }; + + wardrobe( halfU.mul( - 0.82 ), hash( 7.3, 2.1, 9.9 ).greaterThan( 0.4 ), mix( float( 1.7 ), float( 2.3 ), seed ) ); + wardrobe( halfU.mul( 0.82 ), hash( 3.7, 8.4, 1.5 ).greaterThan( 0.4 ), mix( float( 1.7 ), float( 2.3 ), seed2 ) ); + + // curtains hung just inside the glass: drapes drawn part-way in from each side, + // so some windows read open and others half-covered + + // curtain fabric colour, picked per room from a muted domestic palette — creams and + // taupes through warm grey, dusty blue, sage and faded terracotta — with a small + // in-family drift so drawn drapes vary window to window instead of all reading beige + const swatch = ( a, b ) => mix( color( a ), color( b ), seed2 ); + const pick = hash( 22.4, 6.7, 91.2 ).mul( 6 ); // 0..6, one bucket per family + let fabric = swatch( 0xcabfa6, 0xd8cdb8 ); // cream + fabric = select( pick.greaterThan( 1 ), swatch( 0x8a7a64, 0x9b8c72 ), fabric ); // beige / taupe + fabric = select( pick.greaterThan( 2 ), swatch( 0x706a64, 0x837d76 ), fabric ); // warm grey + fabric = select( pick.greaterThan( 3 ), swatch( 0x5f7079, 0x6f818b ), fabric ); // dusty blue + fabric = select( pick.greaterThan( 4 ), swatch( 0x6c7558, 0x79835f ), fabric ); // sage green + fabric = select( pick.greaterThan( 5 ), swatch( 0x8c5a44, 0x9a6a52 ), fabric ); // faded terracotta + const drape = ( bMin, bMax, gate ) => { + + const h = boxHit( bMin, bMax ); + const pleat = fabric.mul( mix( float( 0.78 ), float( 1.12 ), fract( h.p.x.mul( 2.5 ) ) ) ); // soft vertical pleats + consider( h.hit.and( gate ), h.tN, pleat.mul( falloffAt( h.p.z ) ), 0.2 ); // a drape only transmits a little of the room's glow, never out-glowing the interior + + }; + + const cz0 = setback, cz1 = setback.add( 0.12 ); + // drape widths, biased narrow ( squared ) and each capped at half the room width, so + // the two sides only meet — fully curtaining the window — in the rare room where both + // are nearly closed; most rooms read partly open + const sL = smoothstep( 0.3, 1.0, seed ), sR = smoothstep( 0.3, 1.0, seed2 ); + const lw = halfU.mul( sL.mul( sL ) ); // left drape width ( 0 below seed 0.3 ) + const rw = halfU.mul( sR.mul( sR ) ); // right drape width + drape( vec3( halfU.negate(), floorY, cz0 ), vec3( halfU.negate().add( lw ), ceilY, cz1 ), lw.greaterThan( 0.05 ) ); + drape( vec3( halfU.sub( rw ), floorY, cz0 ), vec3( halfU, ceilY, cz1 ), rw.greaterThan( 0.05 ) ); + + // lit rooms read brighter and take on their bulb's colour ( the lights are on ) + const warmth = mix( vec3( 1.0, 1.0, 1.0 ), lightCol, lit.mul( 0.85 ) ); + return vec4( bestCol.mul( warmth ).mul( mix( float( 1.0 ), float( 1.3 ), lit ) ), lit.mul( bestEmit ) ); + +} ); + +/** + * The NYC masonry palette every tower is dressed from ( hex colours ): limestone-dominant + * with terracotta accents. Shared by the single-tower example and {@link CityGenerator}'s + * building material so both stay in sync. + */ +const buildingPalette = [ + 0xa8553c, 0x9c4a34, // terracotta & red brick ( occasional accent ) + 0x8a6a52, 0x7d6450, // warm brick / brownstone ( muted ) + 0xc4a370, 0xb89a6f, 0xc2b183, // buff / tan + 0xc6c0b2, 0xc6c0b2, 0xbdb7a8, 0xd1ccbe, 0xb4afa1, // limestone / pale dressed stone — the common default + 0x9a988f, 0x8b8983, 0xa5a39a, // grey granite / concrete + 0xdbd6cb, // pale glazed ( accent ) + 0x7c868d // steel / glass ( cool accent ) +]; + +/** Picks one {@link buildingPalette} colour ( a hex number ) for a tower from its seed. */ +function pickBuildingColor( seed ) { + + const h = Math.abs( Math.sin( seed * 12.9898 ) * 43758.5453 ); + return buildingPalette[ Math.floor( ( h - Math.floor( h ) ) * buildingPalette.length ) ]; + +} + +/** + * The facade material: a single MeshStandardNodeMaterial that reads the baked + * per-vertex `partId` and reproduces every zone — procedural terracotta brickwork + * on the walls and piers, smooth dressed stone on the window frames and ornament, + * dark glazing, and grey AC units — all dressed with world-space + * weathering. One material covers the whole building ( and a whole city ), which is + * what makes it compute-rasterizer friendly. `buildingBase` is the tower's flat + * masonry colour as a TSL node: pass a `uniform( Color )` for a single tower, or a + * per-fragment palette pick for a city, so the same material dresses both. + */ +function createSkyscraperMaterial( buildingBase = color( 0xc6c0b2 ) ) { + + const soot = color( 0x4a4236 ); + + // broad weathering, all driven from world position so it reads consistently + // across instanced and merged meshes: a slow tonal drift, a fine clay mottle, + // and sooty vertical streaks that pool low down + + const tone = mx_fractal_noise_float( positionWorld.mul( 0.03 ), 2 ).mul( 0.18 ); + const mottle = mx_noise_float( positionWorld.mul( 0.7 ) ).mul( 0.06 ); + const streak = mx_fractal_noise_float( vec3( positionWorld.x.mul( 1.5 ), positionWorld.y.mul( 0.04 ), positionWorld.z.mul( 1.5 ) ), 2 ); + const dirt = smoothstep( - 0.1, 0.45, streak ).mul( smoothstep( 210, 0, positionWorld.y ) ).mul( 0.6 ); + + // procedural terracotta brickwork in running bond, keyed off the BUILDING-LOCAL position + // so the coursing anchors to each tower ( courses from its base, columns at its faces ) + // and lines up with the brick-snapped floor / bay dimensions. courses run up local Y; + // the across-face axis is world XZ projected onto the face tangent, so brick width stays + // constant on every face including the 45° chamfer. the geometry ( pre-bump ) normal is + // used for the bond axis — otherwise colorNode pulls normal computation into its partId + // branch and glass loses its env reflection. + + const brickH = BRICK.height; + const brickL = BRICK.length; + const mortar = 0.025; // joint width, in metres + + const nrm = normalWorldGeometry.abs(); + const across = positionLocal.x.mul( normalWorldGeometry.z ).sub( positionLocal.z.mul( normalWorldGeometry.x ) ); + const rowCoord = positionLocal.y.div( brickH ); + const courseRow = floor( rowCoord ); + const colCoord = across.div( brickL ).add( mod( courseRow, 2 ).mul( 0.5 ) ); // half-brick stagger per row + + // anti-aliased mortar ( the "pristine grid" trick ): the drawn joint never falls below + // the pixel footprint and its opacity fades to keep energy constant, so lines stay crisp + // up close and dissolve far away instead of shimmering. the horizontal derivative comes + // from continuous world X / Z ( weighted by the normal ), not fwidth( across ) which + // would spike where the normal flips at pier edges. + const mU = mortar / ( 2 * brickL ); + const mV = mortar / ( 2 * brickH ); + const ddU = nrm.z.mul( fwidth( positionWorld.x ) ).add( nrm.x.mul( fwidth( positionWorld.z ) ) ).div( brickL ).clamp( 1e-6, 0.5 ); + const ddV = fwidth( rowCoord ).clamp( 1e-6, 0.5 ); + const distU = float( 0.5 ).sub( fract( colCoord ).sub( 0.5 ).abs() ); + const distV = float( 0.5 ).sub( fract( rowCoord ).sub( 0.5 ).abs() ); + const drawU = ddU.max( mU ); + const drawV = ddV.max( mV ); + const lineU = smoothstep( drawU.add( ddU ), drawU.sub( ddU ), distU ).mul( float( mU ).div( drawU ).min( 1 ) ); + const lineV = smoothstep( drawV.add( ddV ), drawV.sub( ddV ), distV ).mul( float( mV ).div( drawV ).min( 1 ) ); + const wallFacing = smoothstep( 0.7, 0.45, nrm.y ); // brick only on vertical walls — not roofs, ledges, cornice tops + const joint = lineU.max( lineV ).mul( wallFacing ); + + const brickKey = uint( courseRow.add( 1 << 16 ) ).mul( uint( 73856093 ) ).bitXor( uint( floor( colCoord ).add( 1 << 16 ) ).mul( uint( 19349663 ) ) ).toVar(); + const brickRnd = ihash( brickKey ); + const brickRnd2 = ihash( brickKey.add( uint( 1 ) ) ); // independent per-brick hash for hue + + // soft brick relief for the bump: each brick is a gently domed mound falling to the + // recessed mortar over a bevel ( distU / distV are the distance to the nearest column / + // course line, 0 at the joint, 0.5 at the centre ), so bricks read rounded rather than + // scratched. the bevel is widened to at least a screen pixel ( from the world-position + // derivative, our stand-in for a mip LOD ) so the edge never goes sub-pixel and shimmers. + const bevel = 0.02; + const texel = fwidth( positionWorld ).length(); // on-screen size of a surface pixel — our hand-rolled LOD + const lodBevel = texel.mul( 1.5 ).max( bevel ); + const brickFace = smoothstep( 0, lodBevel, distU.mul( brickL ) ).mul( smoothstep( 0, lodBevel, distV.mul( brickH ) ) ).mul( wallFacing ); + const reliefHeight = brickFace.mul( 0.008 ); + const rough = mx_noise_float( positionWorld.mul( 0.5 ) ).mul( 0.08 ).add( 0.82 ).add( joint.mul( 0.12 ) ); + + // the merged geometry carries a per-vertex partId; this material reads it and + // branches to reproduce each zone — no per-part materials, compute-raster friendly + + const partId = varying( attribute( 'partId', 'float' ) ).setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER ); // flat: a per-face id must not interpolate, or equal() below misses on the rounding + const isGlass = partId.equal( GLASS ); + const isFrame = partId.equal( FRAME ); + const isOrnament = partId.equal( ORNAMENT ); + const isAC = partId.equal( AC ); + + // stone zones: brick + weathering on the building's colour, lightened for + // piers / ornament and darkened for window frames + const lighten = select( partId.equal( PIER ), float( 0.12 ), select( isOrnament, float( 0.2 ), float( 0 ) ) ); + const perBrick = float( 1 ).add( tone ).add( mottle ).add( brickRnd.sub( 0.5 ).mul( 0.14 ) ); + // per-brick warm/cool shift ( red up / blue down, or vice-versa ) so individual + // bricks read as slightly different fired tones, relative to the building's colour + const warmCool = brickRnd2.sub( 0.5 ).mul( 0.14 ); + const brickShift = vec3( float( 1 ).add( warmCool ), float( 1 ), float( 1 ).sub( warmCool ) ); + const tint = mix( buildingBase, color( 0xffffff ), lighten ).mul( perBrick ).mul( brickShift ); + const masonry = mix( tint, tint.mul( 0.6 ), joint ); // recessed joints read darker + // roofs / ledges show every blotch ( flat & light ), so horizontal surfaces get a gentler, + // larger-scale grime instead of the wall's streaky soot — confined to those surfaces by a + // branch ( roofMask > 0 ), so the fractal never runs on the vertical facade + const roofMask = wallFacing.oneMinus(); + const roofGrime = select( roofMask.greaterThan( 0 ), smoothstep( 0.0, 0.55, mx_fractal_noise_float( positionWorld.mul( 0.025 ), 3 ) ).mul( 0.22 ), float( 0 ) ); + const stoneColor = mix( masonry, soot, mix( dirt, roofGrime, roofMask ) ); + + // glass: the interior-mapped room is the base colour; the smooth, low-roughness + // surface still lets a faint sky reflection ride over it, and lit rooms glow ( emissive ). + // toVar so the raymarch runs once, shared by the colour and emissive outputs + const room = interior().toVar(); + + // grimy glazing: the room shows through, but muted by a dusty film and dirt pooled + // along the bottom of each pane, plus a baseline haze, so the panes read as old + // glass rather than open holes. the streaks run down the facade ( world Y barely + // scaled ); the pooled dirt uses the pane's own UV ( y = 0 at the sill ). + const filmNoise = mx_fractal_noise_float( vec3( positionWorld.x.mul( 1.3 ), positionWorld.y.mul( 0.06 ), positionWorld.z.mul( 1.3 ) ), 2 ); + const dustStreak = smoothstep( - 0.15, 0.5, filmNoise ).mul( 0.45 ); + const pooled = smoothstep( 0.32, 0.0, uv().y ).mul( 0.4 ); + const grime = float( 0.64 ).add( dustStreak ).add( pooled ).clamp( 0, 0.95 ); // baseline haze so the panes read as dirty glass, not open holes + const dirtyGlass = mix( color( 0x13161a ), color( 0x232b31 ), mx_noise_float( positionWorld.mul( 0.3 ) ).mul( 0.5 ).add( 0.5 ) ); + const glassColor = mix( room.xyz.mul( color( 0xb6c6bf ) ), dirtyGlass, grime ); // faint green-grey ( soda-lime ) room tint, dirtied toward grimy glass + + // window frames are smooth dressed stone, not brick + const frameColor = buildingBase.mul( 0.55 ); + + // finials / ornament: smooth dressed stone ( lightened ), not brick + const ornamentColor = mix( buildingBase, color( 0xffffff ), 0.22 ).mul( float( 1 ).add( tone ) ); + // window AC units: a louvered white-plastic box, grimier toward the base where it drips. + // keyed off the box's own UVs ( acUv.y runs 0 → 1 up each vented side ) + const acUv = uv(); + const acVent = smoothstep( 0.65, 0.4, normalWorldGeometry.y.abs() ); // 1 on the vertical vented sides, 0 on the flat top + const acDetail = smoothstep( 0.08, 0.015, texel ); // louvers fade out before a slat nears a pixel + const acLouver = acVent.mul( acDetail ); + + // plastic shell: off-white, some units dingier / yellowed than others + const acDinge = mx_noise_float( positionWorld.mul( 0.4 ) ).mul( 0.5 ).add( 0.5 ); // ~per-unit + const acPaint = mix( color( 0xf2f1ec ), color( 0xcfccc2 ), acDinge ) // bright white → light dingy grey, both lighter than the wall + .add( mx_noise_float( positionWorld.mul( 5 ) ).mul( 0.04 ) ); + + // a darker recessed grille panel inset into the lighter cabinet, with horizontal louvers + // inside it ( the front vents ) — the white plastic reads as a thin border frame + const acGrille = smoothstep( 0.06, 0.14, acUv.x ).mul( smoothstep( 0.94, 0.86, acUv.x ) ) + .mul( smoothstep( 0.12, 0.2, acUv.y ) ).mul( smoothstep( 0.96, 0.88, acUv.y ) ).mul( acLouver ); + const acSlats = fract( acUv.y.mul( 6 ) ); // bold louvers — reads at the unit's small on-screen size + const acFin = mix( float( 0.82 ), float( 1.04 ), acSlats ); + const acBody = acPaint.mul( mix( float( 1 ), acFin.mul( 0.42 ), acGrille ) ); // cabinet stays light; recessed grille goes dark grey + + // grey-brown condensate grime streaking the lower edge ( plastic doesn't rust ); dirtier units streak more + const acStreak = mx_fractal_noise_float( vec3( positionWorld.x.mul( 6 ), positionWorld.y.mul( 0.5 ), positionWorld.z.mul( 6 ) ), 3 ).mul( 0.5 ).add( 0.5 ); + const acGrime = smoothstep( 0.4, 0.0, acUv.y ).mul( acStreak ).mul( acDinge.add( 0.3 ) ); + const acColor = mix( acBody, color( 0x6f685a ), acGrime.mul( 0.5 ) ); + + // recessed grille ( louver fins ) relief and a slightly rougher base + const acRelief = acGrille.mul( acSlats.mul( 0.012 ).sub( 0.01 ) ); + const acRough = float( 0.52 ).add( acGrille.mul( 0.08 ) ); + + const material = new MeshStandardNodeMaterial(); + material.colorNode = select( isGlass, glassColor, select( isFrame, frameColor, select( isOrnament, ornamentColor, select( isAC, acColor, stoneColor ) ) ) ); + material.roughnessNode = select( isGlass, float( 0.18 ), select( isOrnament, float( 0.8 ), select( isAC, acRough, rough ) ) ); // glass kept smooth for a sky reflection, but soft enough not to alias over the interior + material.metalnessNode = float( 0 ); // all dielectric — stone, glass and the plastic AC shells + material.emissiveNode = select( isGlass, room.xyz.mul( room.w ).mul( 4 ).mul( grime.mul( 0.6 ).oneMinus() ), color( 0x000000 ) ); // room.w = emissive weight ( 0 unlit, < 1 behind curtains ), muted further by grime + material.normalNode = bumpNormal( select( isGlass.or( isFrame ).or( isOrnament ), float( 0 ), select( isAC, acRelief, reliefHeight ) ) ); // glass / frames / ornament stay flat; AC has its own louvers + + return material; + +} + +export { SkyscraperGenerator, createSkyscraperMaterial, buildingPalette, pickBuildingColor }; diff --git a/examples/screenshots/webgpu_generator_building.jpg b/examples/screenshots/webgpu_generator_building.jpg new file mode 100644 index 0000000000000000000000000000000000000000..244b80594c09520167680c94e58433e3c5f83825 GIT binary patch literal 17699 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 JGW=rd{{z^E8u|bL literal 0 HcmV?d00001 diff --git a/examples/screenshots/webgpu_generator_city.jpg b/examples/screenshots/webgpu_generator_city.jpg new file mode 100644 index 0000000000000000000000000000000000000000..caf2b0c6c484609453e7aa5edde161e2b4733549 GIT binary patch literal 75918 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 Yx`z0Y`C&HrGvf8z(7&Vq2c4^md;kCd literal 0 HcmV?d00001 diff --git a/examples/webgpu_generator_building.html b/examples/webgpu_generator_building.html new file mode 100644 index 00000000000000..54d94e0dc03c52 --- /dev/null +++ b/examples/webgpu_generator_building.html @@ -0,0 +1,276 @@ + + + + three.js webgpu - building generator + + + + + + + + + + +
+ + +
+ three.jsBuilding Generator +
+ + + A single procedurally generated Neo-Gothic terracotta skyscraper at sunset. +
+ + + + + + + diff --git a/examples/webgpu_generator_city.html b/examples/webgpu_generator_city.html new file mode 100644 index 00000000000000..5f5b054ca7646d --- /dev/null +++ b/examples/webgpu_generator_city.html @@ -0,0 +1,225 @@ + + + + three.js webgpu - city generator + + + + + + + + + + +
+ + +
+ three.jsCity Generator +
+ + + A few procedurally generated city blocks of Neo-Gothic terracotta skyscrapers at sunset. + +
+ + + + + + + diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 53355c48d31cd1..1b2e4fde3b6c51 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -68,7 +68,10 @@ const exceptionList = [ // Webcam 'webgl_materials_video_webcam', - 'webgl_morphtargets_webcam' + 'webgl_morphtargets_webcam', + + // Sub-pixel coverage of thin high-contrast geometry edges differs across rasterizers #33817 + 'webgpu_generator_city' ]; From fc5a1a3adacaab36c26ced0bb9c214b8306fb8a5 Mon Sep 17 00:00:00 2001 From: mrdoob Date: Wed, 24 Jun 2026 18:54:41 +0800 Subject: [PATCH 2/2] Examples: Improve webgpu_custom_fog with terrain and forest generators. (#33873) Co-authored-by: Claude Opus 4.8 (1M context) --- examples/jsm/generators/ForestGenerator.js | 347 ++++++++++++++ examples/jsm/generators/TerrainGenerator.js | 504 ++++++++++++++++++++ examples/screenshots/webgpu_custom_fog.jpg | Bin 37482 -> 36215 bytes examples/webgpu_custom_fog.html | 247 +++++++--- 4 files changed, 1026 insertions(+), 72 deletions(-) create mode 100644 examples/jsm/generators/ForestGenerator.js create mode 100644 examples/jsm/generators/TerrainGenerator.js diff --git a/examples/jsm/generators/ForestGenerator.js b/examples/jsm/generators/ForestGenerator.js new file mode 100644 index 00000000000000..9c4391f42bdb6b --- /dev/null +++ b/examples/jsm/generators/ForestGenerator.js @@ -0,0 +1,347 @@ +import { + BufferAttribute, + Group, + IcosahedronGeometry, + InstancedBufferAttribute, + InstancedMesh, + Object3D, + Vector3 +} from 'three'; + +import { MeshStandardNodeMaterial } from 'three/webgpu'; +import { attribute, color, float, Fn, If, mix, mx_noise_float, normalView, positionLocal, positionView, positionWorld, smoothstep, step, uniform } from 'three/tsl'; + +import { ImprovedNoise } from '../math/ImprovedNoise.js'; +import { mergeVertices } from '../utils/BufferGeometryUtils.js'; + +/** + * Carpets a {@link TerrainGenerator} ( or anything exposing `sampleHeight`, + * `sampleSlope`, `minY`, `maxY` and `parameters.size` ) with a forest of hundreds + * of thousands of trees in a single draw call. + * + * Each tree is the cheapest thing that still reads as a tree: a ~20-face icosphere + * squashed into a tapered teardrop and lumped with a little noise, carrying a baked + * dark-base / bright-top gradient. Tens of triangles each, so a single + * {@link THREE.InstancedMesh} of half a million of them costs one draw call. Trees + * are placed by rejection sampling against ecological rules — a min/max altitude + * band ( above the mist floor, below the snowline ), a slope limit ( none on + * cliffs ) and a low-frequency density mask that opens clearings — then jittered in + * yaw, lean and ( squared-biased ) scale so the stand never reads as copies. + * + * ```js + * const forest = new ForestGenerator( { count: 500000 } ); + * scene.add( forest.build( terrain ) ); + * ``` + */ +class ForestGenerator { + + constructor( parameters = {} ) { + + this.parameters = Object.assign( {}, ForestGenerator.defaults, parameters ); + + // stochastic distance cull ( THREE.Fog-style near / far ): drawn within `from`, gone + // past `to`, the band between thinned by a baked random. live-tunable uniforms. + this.from = uniform( this.parameters.from ); + this.to = uniform( this.parameters.to ); + + // main-camera position ( set via setCameraPosition ). NOT the TSL cameraPosition node: + // in the shadow pass that resolves to the light, which would cull the wrong trees. + this._cameraPosition = uniform( new Vector3() ); + + this.material = createForestMaterial( this.from, this.to, this._cameraPosition ); + this.mesh = null; + this.group = null; + + } + + build( terrain ) { + + this.dispose(); + + const p = this.parameters; + const geometry = blobGeometry( p ); + + const size = terrain.parameters.size; + const minY = terrain.minY; + const span = terrain.maxY - terrain.minY; + + const random = createRandom( p.seed ); + + // a low-frequency field that breaks the forest into patches and clearings + const perlin = new ImprovedNoise(); + const dOffX = random() * 256, dOffZ = random() * 256, dSlice = random() * 256; + const densityAt = ( x, z ) => smoothBlend( - 0.12, 0.22, perlin.noise( x * p.densityFrequency + dOffX, z * p.densityFrequency + dOffZ, dSlice ) ); + + const mesh = new InstancedMesh( geometry, this.material, p.count ); + mesh.castShadow = mesh.receiveShadow = p.castShadow; // honoured on every rebuild + + // per-instance cull data: xyz = tree position ( for its distance to the camera ), + // w = a threshold jitter from a separate PRNG, so it doesn't disturb placement + const cullData = new Float32Array( p.count * 4 ); + const cullRandom = createRandom( ( p.seed ^ 0x9e3779b9 ) >>> 0 ); + + // per-instance regional colour drift, baked here so the vertex-bound shader taps no + // noise. offsets come from the cull PRNG, so placement is untouched. + const regionData = new Float32Array( p.count ); + const rOffX = cullRandom() * 256, rOffZ = cullRandom() * 256, rSlice = cullRandom() * 256; + + const dummy = new Object3D(); + let placed = 0; + let attempts = 0; + const maxAttempts = p.count * 14; // give up rather than hang if the band is too small + + while ( placed < p.count && attempts < maxAttempts ) { + + attempts ++; + + const x = ( random() - 0.5 ) * size; + const z = ( random() - 0.5 ) * size; + + const y = terrain.sampleHeight( x, z ); + const altitude = ( y - minY ) / span; + if ( altitude < p.altitudeMin || altitude > p.altitudeMax ) continue; + + if ( terrain.sampleSlope( x, z ) < p.minSlope ) continue; + + // density mask, feathered out at the top so the treeline scatters, not a clean line + let density = densityAt( x, z ); + density *= smoothBlend( p.altitudeMax, p.altitudeMax - 0.14, altitude ); + if ( random() >= density ) continue; + + dummy.position.set( x, y - p.sink, z ); // sink the base point into the ground + dummy.rotation.set( ( random() - 0.5 ) * 0.12, random() * Math.PI * 2, ( random() - 0.5 ) * 0.12 ); // small lean + free yaw, trunk ~vertical + + const s = p.minScale + random() * random() * ( p.maxScale - p.minScale ); // squared bias: mostly small, rare giants + dummy.scale.set( s * ( 0.85 + random() * 0.3 ), s, s * ( 0.85 + random() * 0.3 ) ); + + dummy.updateMatrix(); + mesh.setMatrixAt( placed, dummy.matrix ); + + const c = placed * 4; + cullData[ c ] = x; + cullData[ c + 1 ] = dummy.position.y; // the sunk y, matching the drawn position + cullData[ c + 2 ] = z; + cullData[ c + 3 ] = cullRandom(); + + regionData[ placed ] = Math.min( 1, Math.max( 0, perlin.noise( x * 0.02 + rOffX, z * 0.02 + rOffZ, rSlice ) * 0.6 + 0.5 ) ); + + placed ++; + + } + + mesh.count = placed; // only what got planted + mesh.instanceMatrix.needsUpdate = true; + geometry.setAttribute( 'cull', new InstancedBufferAttribute( cullData, 4 ) ); + geometry.setAttribute( 'region', new InstancedBufferAttribute( regionData, 1 ) ); + + const group = new Group(); + group.name = 'Forest'; + group.add( mesh ); + + this.mesh = mesh; + this.group = group; + + return group; + + } + + // call each frame so the distance cull tracks the camera + setCameraPosition( position ) { + + this._cameraPosition.value.copy( position ); + + } + + dispose() { + + if ( this.mesh ) this.mesh.geometry.dispose(); + this.mesh = null; + this.group = null; + + } + +} + +ForestGenerator.defaults = { + seed: 1, + count: 500000, // number of trees to plant ( a single instanced draw call ) + detail: 0, // icosphere subdivision ( 0 = 20 faces, welds to 12 verts ) + radius: 1.3, // base half-width of a tree blob, in world units + height: 4, // base height of a tree blob + distortion: 0.5, // lumpiness of the blob hull ( a rough conifer, not a smooth egg ) + sink: 0.4, // how far the base point is pushed under the surface, to hide it + altitudeMin: 0.12, // normalised altitude band the forest occupies: above the mist floor... + altitudeMax: 0.46, // ...and safely below the snowline + minSlope: 0.55, // minimum surface flatness ( normal.y ); steeper ground stays bare rock + densityFrequency: 0.012, // patch / clearing scale ( world units ) + minScale: 0.7, + maxScale: 1.8, + from: 300, // distance ( like THREE.Fog ) within which every tree is drawn... + to: 620, // ...past which none are; the band between thins out stochastically + castShadow: false // whether the canopy casts + receives shadows ( 500k casters is a real cost — opt in ) +}; + +// deterministic PRNG ( mulberry32 ), matching the other generators +function createRandom( seed ) { + + let s = ( seed >>> 0 ) || 1; + + return function () { + + s = ( s + 0x6D2B79F5 ) | 0; + let t = Math.imul( s ^ ( s >>> 15 ), 1 | s ); + t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t; + return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296; + + }; + +} + +function smoothBlend( edge0, edge1, x ) { + + const t = Math.max( 0, Math.min( 1, ( x - edge0 ) / ( edge1 - edge0 ) ) ); + return t * t * ( 3 - 2 * t ); + +} + +// smooth low-frequency lump over the unit sphere, so the blob hull is bumpy not spiky +function blobNoise( x, y, z ) { + + return Math.sin( x * 3.1 ) * Math.sin( y * 2.7 + 1.3 ) * Math.sin( z * 3.5 + 2.1 ); + +} + +// one tree blob: an icosphere squashed into a lumpy, tapered teardrop, base at y = 0. +// normals are re-pointed up-and-out so it shades as a soft canopy volume; a baked `ao` +// ( 0 base → 1 crown ) drives the dark-underside / bright-crown gradient. +function blobGeometry( p ) { + + // IcosahedronGeometry is non-indexed ( 60 verts ); deleting uv + normal lets mergeVertices + // weld by position to 12 verts — ~5× fewer vertex-shader runs. normals are rebuilt below. + let geometry = new IcosahedronGeometry( 1, p.detail ); + geometry.deleteAttribute( 'uv' ); + geometry.deleteAttribute( 'normal' ); + geometry = mergeVertices( geometry ); + + const position = geometry.attributes.position; + const count = position.count; + + const normals = new Float32Array( count * 3 ); + const ao = new Float32Array( count ); + + for ( let i = 0; i < count; i ++ ) { + + const ux = position.getX( i ); + const uy = position.getY( i ); + const uz = position.getZ( i ); // a point on the unit sphere + + const h = ( uy + 1 ) / 2; // 0 at the base, 1 at the top + const taper = 1 - 0.62 * h; // narrower toward a pointier crown + const lump = 1 + p.distortion * blobNoise( ux, uy, uz ); + const r = taper * lump; + + position.setXYZ( i, ux * r * p.radius, h * p.height, uz * r * p.radius ); + + // up-and-outward normal: a soft, dome-lit canopy rather than faceted rock + const inv = 1 / Math.hypot( ux, 0.55, uz ); + normals[ i * 3 ] = ux * inv; + normals[ i * 3 + 1 ] = 0.55 * inv; + normals[ i * 3 + 2 ] = uz * inv; + + ao[ i ] = h; + + } + + position.needsUpdate = true; + geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); + geometry.setAttribute( 'ao', new BufferAttribute( ao, 1 ) ); + geometry.computeBoundingSphere(); + + return geometry; + +} + +// derivative-based bump ( surface-gradient method ): perturbs the view normal from a +// procedural height field, so the canopy reads as clustered foliage, not a smooth shell +function bumpNormal( height ) { + + const dpdx = positionView.dFdx(); + const dpdy = positionView.dFdy(); + const r1 = dpdy.cross( normalView ); + const r2 = normalView.cross( dpdx ); + const det = dpdx.dot( r1 ); + const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) ); + + return det.abs().mul( normalView ).sub( grad ).normalize(); + +} + +/** + * The single material shared by every tree in a {@link ForestGenerator}. A plain + * MeshStandardNodeMaterial lit by the scene — only the surface is authored: deep + * shadowed green in the recesses rising to a bright, yellow-green sunlit crown, + * mottled into needle clumps by 3D noise, with a matching bump so the clumps catch + * the light. Half a million instanced blobs makes this mesh vertex-bound, so the + * regional colour drift is baked to a per-instance attribute ( no shader noise for it ), + * and the costly clump noise + bump are **gated by distance** — full detail on the near + * trees ( where it reads ), skipped on the far canopy ( where it is sub-pixel ). + * + * @param {Node} from - distance within which every tree is drawn. + * @param {Node} to - distance past which no tree is drawn. + * @return {MeshStandardNodeMaterial} + */ +function createForestMaterial( from, to, camPos ) { + + const material = new MeshStandardNodeMaterial(); + material.metalness = 0; + material.roughness = 0.88; + + const cull = attribute( 'cull', 'vec4' ); // xyz = tree position, w = random 0..1 + const d = cull.xyz.distance( camPos ); // per-tree distance to the ( main ) camera + + // stochastic distance cull: past its jittered `from`→`to` threshold a tree collapses to a + // point, dropping the far canopy. `positionLocal` is already WORLD space here ( the instance + // transform runs before positionNode ), so the ×0 lands the whole blob on the origin. + const t = d.sub( from ).div( to.sub( from ) ); + material.positionNode = positionLocal.mul( step( t, cull.w ) ); // keep where random ≥ t + + const ao = attribute( 'ao', 'float' ); // 0 at the blob base, 1 at the crown + + // regional drift, baked per tree ( see build ) so no stage taps a noise; a blob is small + // enough that one value per tree reads as a smooth field across the canopy + const region = attribute( 'region', 'float' ); + const deep = mix( color( 0x1d3318 ), color( 0x2e4420 ), region ); // shadowed interior + const bright = mix( color( 0x4c6a2e ), color( 0x6e8a40 ), region ); // sunlit tips ( muted green, not neon ) + + // one 3D noise field ( coarse + fine ), shared by the colour and bump, near canopy only + const detailFade = smoothstep( 280, 25, positionWorld.distance( camPos ) ); + + // gated by an If ( which must sit inside an Fn ) so the far canopy skips the noise + const clump = Fn( () => { + + const c = float( 0 ).toVar(); + + If( detailFade.greaterThan( 0.01 ), () => { + + c.assign( mx_noise_float( positionWorld.mul( 0.9 ) ) + .add( mx_noise_float( positionWorld.mul( 3.1 ) ).mul( 0.5 ) ) + .mul( detailFade ) ); + + } ); + + return c; + + } )(); + + // deep recesses → bright clumps / crown + const lit = ao.mul( 0.5 ).add( 0.32 ).add( clump.mul( 0.18 ) ).clamp(); + material.colorNode = mix( deep, bright, lit ); + + // clumps catch the light ( clump is 0 far away, so the bump flattens there ) + material.normalNode = bumpNormal( clump.mul( 0.22 ) ); + + return material; + +} + +export { ForestGenerator, createForestMaterial }; diff --git a/examples/jsm/generators/TerrainGenerator.js b/examples/jsm/generators/TerrainGenerator.js new file mode 100644 index 00000000000000..dc7bc304ee2030 --- /dev/null +++ b/examples/jsm/generators/TerrainGenerator.js @@ -0,0 +1,504 @@ +import { + BufferGeometry, + Float32BufferAttribute, + Group, + Mesh +} from 'three'; + +import { MeshStandardNodeMaterial } from 'three/webgpu'; +import { cameraPosition, color, float, Fn, If, mix, mx_noise_float, normalView, normalWorld, positionView, positionWorld, saturation, smoothstep, uniform } from 'three/tsl'; + +import { ImprovedNoise } from '../math/ImprovedNoise.js'; + +/** + * Bakes a procedural mountain range into a single {@link THREE.BufferGeometry} and + * returns a `THREE.Group` ready to add to a scene. + * + * The heightfield is a derivative-damped fractal sum ( Quilez's fake erosion ): each + * octave is suppressed where the running slope is already steep, concentrating detail + * into weathered ridgelines, and a low-frequency domain warp makes those ridges + * meander. A few passes of thermal ( talus ) erosion then relax any slope past the + * angle of repose, settling the fractal's needle-spikes into real crests. + * + * The grid is triangulated with alternating quad diagonals ( a diamond pattern ), so a + * coarse mesh holds its silhouette without a one-way grain. The surface shades itself + * from altitude and slope in TSL — grass, forest, rock, scree and snow, with detail + * normals and aerial perspective — so no material or textures are needed. + * + * The baked height grid is exposed through {@link TerrainGenerator#sampleHeight} so a + * scattered forest ( or anything else ) can sit exactly on the surface. + * + * ```js + * const terrain = new TerrainGenerator( { seed: 1 } ); + * scene.add( terrain.build() ); + * ``` + */ +class TerrainGenerator { + + constructor( parameters = {} ) { + + this.parameters = Object.assign( {}, TerrainGenerator.defaults, parameters ); + + // baked altitude range, fed to the shader so the colour bands track the real + // valley floor and peaks + this.minHeight = uniform( 0 ); + this.maxHeight = uniform( 1 ); + + this.material = terrainMaterial( this.minHeight, this.maxHeight ); + this.geometry = null; + this.group = null; + + } + + build() { + + this.dispose(); + + const p = this.parameters; + const N = p.segments + 1; + const half = p.size / 2; + + // world coordinate of each grid line, shared by the bake and layout below + const coord = new Array( N ); + for ( let i = 0; i < N; i ++ ) coord[ i ] = i / p.segments * p.size - half; + + // bake the height grid; kept around so the surface can be sampled ( bilinearly ) + // afterwards — e.g. to sit a scattered forest on it + const height = heightField( p ); + const heights = new Float32Array( N * N ); + + for ( let iz = 0; iz < N; iz ++ ) { + + for ( let ix = 0; ix < N; ix ++ ) { + + heights[ iz * N + ix ] = height( coord[ ix ], coord[ iz ] ); + + } + + } + + // relax slopes past the angle of repose, shedding the fractal's needle-spikes + if ( p.talusPasses > 0 ) thermalErode( heights, N, p.size / p.segments, p.talus, p.talusPasses ); + + // lay the grid out flat in the XZ plane ( Y-up ) and find the height range + const positions = new Float32Array( N * N * 3 ); + let min = Infinity, max = - Infinity; + + for ( let iz = 0; iz < N; iz ++ ) { + + for ( let ix = 0; ix < N; ix ++ ) { + + const o = iz * N + ix; + const y = heights[ o ]; + + positions[ o * 3 ] = coord[ ix ]; + positions[ o * 3 + 1 ] = y; + positions[ o * 3 + 2 ] = coord[ iz ]; + + if ( y < min ) min = y; + if ( y > max ) max = y; + + } + + } + + // flip the quad diagonal on every other quad, so the mesh reads as diamonds + // rather than a one-way grain + const indices = []; + + for ( let iz = 0; iz < p.segments; iz ++ ) { + + for ( let ix = 0; ix < p.segments; ix ++ ) { + + const a = iz * N + ix, b = a + 1, c = a + N, d = c + 1; + + if ( ( ix + iz ) % 2 === 0 ) indices.push( a, c, b, b, c, d ); + else indices.push( a, c, d, a, d, b ); + + } + + } + + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); + geometry.setIndex( indices ); + geometry.computeVertexNormals(); + + this.heights = heights; + this.gridSize = N; + this.minY = min; + this.maxY = max; + this.minHeight.value = min; + this.maxHeight.value = max; + + const mesh = new Mesh( geometry, this.material ); + mesh.castShadow = mesh.receiveShadow = true; + + const group = new Group(); + group.name = 'Terrain'; + group.add( mesh ); + + this.geometry = geometry; + this.group = group; + + return group; + + } + + // world-space height at ( x, z ), bilinearly interpolated from the baked grid + sampleHeight( x, z ) { + + const p = this.parameters; + const N = this.gridSize; + const half = p.size / 2; + + const fx = Math.max( 0, Math.min( p.segments, ( x + half ) / p.size * p.segments ) ); + const fz = Math.max( 0, Math.min( p.segments, ( z + half ) / p.size * p.segments ) ); + + const ix = Math.min( N - 2, Math.floor( fx ) ); + const iz = Math.min( N - 2, Math.floor( fz ) ); + const tx = fx - ix; + const tz = fz - iz; + + const h = this.heights; + const h00 = h[ iz * N + ix ]; + const h10 = h[ iz * N + ix + 1 ]; + const h01 = h[ ( iz + 1 ) * N + ix ]; + const h11 = h[ ( iz + 1 ) * N + ix + 1 ]; + + return ( h00 * ( 1 - tx ) + h10 * tx ) * ( 1 - tz ) + ( h01 * ( 1 - tx ) + h11 * tx ) * tz; + + } + + // surface flatness at ( x, z ): the normal's y component ( 1 on the flat, → 0 on a + // cliff ). allocation-free, for cheaply testing many candidate forest positions. + sampleSlope( x, z ) { + + const e = this.parameters.size / this.parameters.segments; + const hx = this.sampleHeight( x + e, z ) - this.sampleHeight( x - e, z ); + const hz = this.sampleHeight( x, z + e ) - this.sampleHeight( x, z - e ); + + return 2 * e / Math.sqrt( hx * hx + 4 * e * e + hz * hz ); + + } + + dispose() { + + if ( this.geometry ) this.geometry.dispose(); + this.geometry = null; + this.group = null; + + } + +} + +TerrainGenerator.defaults = { + seed: 1, + size: 200, // world units across the square patch + segments: 192, // grid quads per side; vertices = ( segments + 1 )² + heightScale: 65, // peak-to-valley exaggeration, in world units + frequency: 0.01, // base noise frequency ( the footprint of a mountain ) + octaves: 5, + lacunarity: 1.97, // per-octave frequency step; off 2 so octaves don't grid-lock + gain: 0.5, // per-octave amplitude step ( persistence ) + erosion: 0.7, // derivative damping: higher flattens valleys and sharpens ridges + warp: 0.35, // domain-warp strength ( noise units ): bends ridges and valleys + valleyBias: 1.2, // power curve over the height, to flatten the mist floor + seaLevel: 0.15, // 0..1, subtracted before scaling so the valley floor sinks below y = 0 + talus: 1, // thermal-erosion angle of repose ( rise / run ): lower settles flatter + talusPasses: 12 // thermal-erosion iterations ( 0 = off ) +}; + +// deterministic PRNG ( mulberry32 ), so a seed always bakes the same terrain +function createRandom( seed ) { + + let s = ( seed >>> 0 ) || 1; + + return function () { + + s = ( s + 0x6D2B79F5 ) | 0; + let t = Math.imul( s ^ ( s >>> 15 ), 1 | s ); + t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t; + return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296; + + }; + +} + +// builds the height( worldX, worldZ ) function for one seed +function heightField( p ) { + + const perlin = new ImprovedNoise(); + const random = createRandom( p.seed ); + + // ImprovedNoise's permutation is fixed, so a seed can only shift the sample window: + // a translation and a per-octave z-slice, drawn from the PRNG to decorrelate seeds + const offsetX = random() * 256; + const offsetZ = random() * 256; + const slice = random() * 256; + + const { frequency, octaves, lacunarity, gain, erosion, warp, valleyBias, seaLevel, heightScale } = p; + + // low-frequency fractal sum that warps the sample position + function warpField( x, z, zr ) { + + let freq = 1, amp = 1, sum = 0, norm = 0; + + for ( let i = 0; i < 2; i ++ ) { + + sum += amp * perlin.noise( x * freq + offsetX, z * freq + offsetZ, zr + i * 1.7 ); + norm += amp; freq *= lacunarity; amp *= gain; + + } + + return sum / norm; + + } + + // derivative-damped fractal sum: each octave is divided down where the running + // gradient is already steep, keeping ridges crisp and valleys smooth. the domain + // rotates between octaves to break the noise's axis-aligned grid. + function eroded( x, z ) { + + let sum = 0, amp = 1, dX = 0, dZ = 0, px = x, pz = z, freq = 1; + const e = 0.004; // finite-difference step, in noise units + + for ( let i = 0; i < octaves; i ++ ) { + + const zr = slice + i * 1.7; + const bx = px * freq + offsetX, bz = pz * freq + offsetZ; + const n = perlin.noise( bx, bz, zr ); + const nx = perlin.noise( bx + e, bz, zr ); + const nz = perlin.noise( bx, bz + e, zr ); + + // this octave's world-space gradient ( chain rule: × freq ) + dX += ( nx - n ) / e * freq; + dZ += ( nz - n ) / e * freq; + + sum += amp * n / ( 1 + erosion * ( dX * dX + dZ * dZ ) ); + + // rotate the domain ~37° ( the matrix [ 0.8 -0.6 ; 0.6 0.8 ] ) + const rx = 0.8 * px - 0.6 * pz; + pz = 0.6 * px + 0.8 * pz; + px = rx; + + freq *= lacunarity; amp *= gain; + + } + + return sum * 0.5 + 0.5; + + } + + return function ( worldX, worldZ ) { + + const x = worldX * frequency, z = worldZ * frequency; + + // warp the sample so ridges and valleys meander instead of running straight + const wx = x + warp * warpField( x + 1.3, z + 7.2, slice + 40 ); + const wz = z + warp * warpField( x + 5.2, z + 1.3, slice + 70 ); + + // power curve that settles the low ground into a flat mist bed + const h = Math.pow( Math.min( eroded( wx, wz ) * 1.1, 1 ), valleyBias ); + + return ( h - seaLevel ) * heightScale; + + }; + +} + +// thermal ( talus ) erosion on the baked height grid: a cell overhanging a neighbour +// by more than the talus drop sheds the excess downhill, so over a few passes slopes +// relax to the angle of repose. spikes — steep on every side — bleed off fastest; +// broad one-sided faces keep their shape. material is conserved through a delta buffer, +// so the result is independent of cell order. +function thermalErode( h, N, cellSize, talus, passes ) { + + const drop = talus * cellSize; // max height step a slope can hold between two cells + const carry = 0.5; // fraction of the steepest overhang moved per pass ( <= 0.5 = stable ) + const delta = new Float32Array( N * N ); + const ex = [ 0, 0, 0, 0 ]; + const off = [ - 1, 1, - N, N ]; + + for ( let p = 0; p < passes; p ++ ) { + + delta.fill( 0 ); + + for ( let z = 0; z < N; z ++ ) { + + for ( let x = 0; x < N; x ++ ) { + + const i = z * N + x; + const hi = h[ i ]; + + // overhang past the talus drop toward each of the 4 neighbours + ex[ 0 ] = x > 0 ? hi - h[ i - 1 ] - drop : 0; + ex[ 1 ] = x < N - 1 ? hi - h[ i + 1 ] - drop : 0; + ex[ 2 ] = z > 0 ? hi - h[ i - N ] - drop : 0; + ex[ 3 ] = z < N - 1 ? hi - h[ i + N ] - drop : 0; + + let sum = 0, peak = 0; + + for ( let k = 0; k < 4; k ++ ) { + + const d = ex[ k ]; + + if ( d <= 0 ) { + + ex[ k ] = 0; + continue; + + } + + sum += d; + if ( d > peak ) peak = d; + + } + + if ( sum <= 0 ) continue; + + // move a slice of the steepest overhang, split across the downhill + // neighbours in proportion to how far each sits below the talus line + const move = carry * peak; + delta[ i ] -= move; + + for ( let k = 0; k < 4; k ++ ) { + + if ( ex[ k ] > 0 ) delta[ i + off[ k ] ] += move * ex[ k ] / sum; + + } + + } + + } + + for ( let k = 0; k < N * N; k ++ ) h[ k ] += delta[ k ]; + + } + +} + +// --- shading ------------------------------------------------------------- + +// perturbs the normal by a world-space height field using Mikkelsen's surface-gradient +// method. the built-in bumpMap reads height by offsetting the UV — a no-op for a +// world-keyed height — so the height's screen-space derivatives are fed in directly. +// returns a view-space normal. +function bumpNormal( height ) { + + const dpdx = positionView.dFdx(); + const dpdy = positionView.dFdy(); + const r1 = dpdy.cross( normalView ); + const r2 = normalView.cross( dpdx ); + const det = dpdx.dot( r1 ); + const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) ); + + return det.abs().mul( normalView ).sub( grad ).normalize(); + +} + +// altitude- and slope-based shading, all in TSL ( no textures ). only the colour, +// roughness and detail normal are authored here; the lighting ( sun, sky fill, the +// snow's warm/cool cast ) comes from the scene's lights and environment. +function terrainMaterial( minHeight, maxHeight ) { + + const material = new MeshStandardNodeMaterial(); + material.metalness = 0; + + const distance = positionWorld.distance( cameraPosition ); + + // the two drivers: normalised altitude ( valley 0 → peak 1 ) and surface flatness + const altitude = positionWorld.y.sub( minHeight ).div( maxHeight.sub( minHeight ) ).clamp(); + const flatness = normalWorld.y.clamp(); // 1 on level ground, 0 on a vertical cliff + const steep = flatness.oneMinus(); + + // three reused noise scales: fine band-edge jitter, grain ( ~5u patches ) and macro + const detail = mx_noise_float( positionWorld.xz.mul( 0.05 ) ); + const grain = mx_noise_float( positionWorld.xz.mul( 0.18 ) ); + const macro = mx_noise_float( positionWorld.xz.mul( 0.012 ) ); + + const grass = color( 0x6e7253 ); // dry sage-olive meadow ( not video-game green ) + const dryGrass = color( 0x8a8550 ); + const forest = color( 0x39402f ); // dark forested mid-slope band, under the trees + const rock = color( 0x736a5f ); // warm grey-brown rock + const scree = color( 0x837a6f ); // brighter broken rock below the cliffs + const lichen = color( 0x6c7355 ); // muted green-grey, patched onto lower rock + const snow = color( 0xe9ecf0 ); // fresh snow; warm-sun / cool-sky cast is from the lighting + const snowDeep = color( 0xccd6e2 ); // cooler wind-packed snow, drifted into patches + + // two band frequencies of lighter / darker stone, wobbled by noise, so cliff faces + // read as layered bedding instead of flat grey + const bandA = positionWorld.y.mul( 0.5 ).add( detail.mul( 3 ) ).add( macro.mul( 4 ) ).sin(); + const bandB = positionWorld.y.mul( 1.4 ).add( grain.mul( 2 ) ).sin(); + const strata = bandA.mul( 0.6 ).add( bandB.mul( 0.4 ) ).mul( 0.5 ).add( 0.5 ); + + // lichen creeps onto the lower, gentler rock; cliffs and high ground stay bare grey + const lichenMask = smoothstep( 0.45, 0.72, grain ).mul( smoothstep( 0.62, 0.32, steep ) ).mul( smoothstep( 0.66, 0.34, altitude ) ); + const rockShade = mix( rock, lichen, lichenMask.mul( 0.45 ) ).mul( strata.mul( 0.36 ).add( 0.8 ) ); + + // meadow, drifting to dry grass in macro-noise patches over a mid band + let surface = mix( grass, dryGrass, smoothstep( 0.15, 0.75, macro ).mul( smoothstep( 0.22, 0.5, altitude ) ) ); + + // dark forested band on the gentle mid-slopes ( where the instanced trees live ) + surface = mix( surface, forest, smoothstep( 0.16, 0.34, altitude ).mul( smoothstep( 0.5, 0.72, flatness ) ).mul( 0.75 ) ); + + // rock by altitude, and on every steep face regardless of height + surface = mix( surface, rockShade, smoothstep( 0.46, 0.64, altitude.add( detail.mul( 0.06 ) ) ) ); + surface = mix( surface, rockShade, smoothstep( 0.34, 0.62, steep ) ); + + // scree on the medium-steep ground below the cliffs, broken up by noise + const screeMask = smoothstep( 0.42, 0.7, steep ).mul( smoothstep( 0.35, 0.7, flatness ) ).mul( detail.mul( 0.5 ).add( 0.5 ) ); + surface = mix( surface, scree, screeMask.mul( 0.5 ) ); + + // snow on high, flat ground; the grain noise breaks the line so rock pokes through + // near the snowline instead of stopping on a clean contour + const snowMask = smoothstep( 0.56, 0.78, altitude.add( detail.mul( 0.08 ) ).add( grain.mul( 0.05 ) ) ).mul( smoothstep( 0.3, 0.6, flatness ) ); + const snowColor = mix( snow, snowDeep, smoothstep( 0.2, 0.7, grain ).mul( 0.6 ) ); // patchy, not a flat sheet + surface = mix( surface, snowColor, snowMask ); + + // dark, damp ground pooling in the low flat creases ( cheap moisture proxy ) + const cavity = smoothstep( 0.24, 0.06, altitude ).mul( flatness ); + surface = surface.mul( cavity.mul( 0.32 ).oneMinus() ); + + // macro drift then a fine grain mottle, so no band is a flat colour + surface = surface.mul( macro.mul( 0.5 ).add( 0.5 ).mul( 0.3 ).add( 0.84 ) ); + surface = surface.mul( grain.mul( 0.5 ).add( 0.5 ).mul( 0.12 ).add( 0.94 ) ); + + // aerial perspective: desaturate and lift distant ground toward a cool haze, so + // depth reads and the range recedes into the mist + const aerial = smoothstep( 180, 820, distance ); + surface = saturation( surface, aerial.oneMinus().mul( 0.5 ).add( 0.5 ) ); + surface = mix( surface, color( 0xcfc8ba ), aerial.mul( 0.62 ) ); // far ridges dissolve into the sky + + material.colorNode = surface; + material.roughnessNode = mix( float( 0.95 ), float( 0.72 ), snowMask ); + + // detail normals: three octaves of world-space relief, faded out with distance so + // they can't alias into fireflies in the haze. gating the noise behind the fade ( a + // real branch ) lets the far majority of this fragment-bound terrain skip the taps. + const detailFade = smoothstep( 420, 60, distance ); + const reliefStrength = mix( float( 0.25 ), float( 0.55 ), steep ); // more on rock, less on grass + const relief = Fn( () => { + + const r = float( 0 ).toVar(); + + If( detailFade.greaterThan( 0.01 ), () => { + + r.assign( mx_noise_float( positionWorld.xz.mul( 0.6 ) ) + .add( mx_noise_float( positionWorld.xz.mul( 1.7 ) ).mul( 0.5 ) ) + .add( mx_noise_float( positionWorld.xz.mul( 4.0 ) ).mul( 0.25 ) ) + .mul( reliefStrength ).mul( detailFade ).mul( 0.25 ) ); + + } ); + + return r; + + } )(); + + material.normalNode = bumpNormal( relief ); + + return material; + +} + +export { TerrainGenerator }; diff --git a/examples/screenshots/webgpu_custom_fog.jpg b/examples/screenshots/webgpu_custom_fog.jpg index fe4ef73ea90393c0eb6547e6f6dc37697afe11c6..d05b3d73af1eaede5c4b5778fbdecf5c115e299c 100644 GIT binary patch literal 36215 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%0AJVjupExF;>EbX#7fT`p_y(< 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 E+0ucJ1poj5 delta 37017 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πqQ0R7*?Z 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 BSpNV3 diff --git a/examples/webgpu_custom_fog.html b/examples/webgpu_custom_fog.html index 04ad685e9f4ec2..4b865ec801d4b3 100644 --- a/examples/webgpu_custom_fog.html +++ b/examples/webgpu_custom_fog.html @@ -9,6 +9,9 @@ + @@ -20,7 +23,7 @@ - Custom Fog via TSL. + Custom height fog via TSL, pooling in a procedural alpine valley forested with 500,000 instanced trees. @@ -38,24 +41,67 @@