ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

three.js CityGenerator 详解:种子驱动的程序化城市、逐地块塔楼与街道材质

2026/9/7 7:40:22 拓冰建站 浏览量
three.js CityGenerator 详解:种子驱动的程序化城市、逐地块塔楼与街道材质 three.js CityGenerator 详解种子驱动的程序化城市、逐地块塔楼与街道材质【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.jsCityGenerator 是 three.js 示例模块中的一个程序化城市生成器它在网格上排布城市街区block为每块建筑用地lot生成一座拥有独立种子、高度和占地尺寸的摩天楼可选地在抬高的圆角人行道板路缘石之上并返回一个可直接加入场景的THREE.Group。读完本篇你将掌握它的全部默认参数与布局推导公式、build()的构建流程与销毁生命周期、确定性随机机制以及如何用仓库自带的createBuildingMaterial/createRoadMaterial让整座城市共享一套 TSL 程序化材质并把路面标线对齐到城市网格。1. CityGenerator 解决什么问题文档页对该模块的官方描述非常凝练Lays out a grid of city blocks and fills each lot with a SkyscraperGenerator tower of its own seed, height and footprint, optionally on raised sidewalk slabs (curbs). Returns aTHREE.Groupready to add to a scene.即输入一个城市布局参数街区数量、用地尺寸、街道宽度等自动完成「街区 → 用地 → 建筑」三级网格划分每栋塔楼由 SkyscraperGenerator 独立生成拥有自己的seed、总高与占地互不相同当curbHeight 0时每个街区会垫在由 SidewalkGenerator 生成的圆角人行道板路缘石上调用build( materials )得到一个名为City的THREE.Group直接scene.add()即可。材质职责是明确分工的塔楼材质由调用方传入materials.building而人行道材质由 SidewalkGenerator 自己创建并持有浇筑混凝土板块 花岗岩路缘石的程序化 TSL。布局结果则通过city.layout属性对外暴露文档特别指出这一点是有意为之The layout is exposed as CityGenerator#layout so the surrounding scene (road markings, etc.) can align to the same grid.也就是说路面标线、人行横道等周边元素可以读取同一份布局数据保证与建筑网格严格对齐——这在第 7 节的完整示例中会看到实际用法。2. 快速上手文档给出的最小用法是const city new CityGenerator( { seed: 1 } ); scene.add( city.build( materials ) );其中materials是一个对象当前只认building一个键build()内部把materials.building原样传给每一座SkyscraperGenerator作为塔楼材质。如果不想自己写塔楼材质仓库导出了配套的createBuildingMaterial( layout, seed )可用一份材质给整片天际线上色原理见第 5 节import { CityGenerator, createBuildingMaterial, createRoadMaterial } from three/addons/generators/CityGenerator.js; const city new CityGenerator( { seed: 94 } ); const materials { building: createBuildingMaterial( city.layout, 94 ) }; scene.add( city.build( materials ) );适用前提CityGenerator.js头部从three/webgpu导入MeshStandardNodeMaterial、从three/tsl导入大量 TSL 节点源码第 6–7 行因此createBuildingMaterial/createRoadMaterial这两个 TSL 材质工厂依赖 WebGPU 渲染器环境仓库中的完整示例 webgpu_generator_city.html 正是跑在WebGPURenderer上。而CityGenerator本体的几何生成只用到Group、Matrix4等核心 API结构上与渲染后端无关。3. 构造函数与默认参数文档中 Constructor 小节仅声明new 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; }构造函数做三件事用Object.assign把调用方参数覆盖到CityGenerator.defaults上调用cityLayout()一次性推导全部布局尺寸并保存到this.layout按布局尺寸预创建人行道生成器人行道板尺寸恰好等于单个街区尺寸blockW × blockD高度即curbHeight圆角半径即curbRadius。默认值定义在 CityGenerator.defaults参数默认值含义seed1布局随机种子同一种子永远生成同一座城市street22街道宽度世界单位lot30单个建筑用地边长lotsX3每个街区沿 X 方向的用地数lotsZ2每个街区沿 Z 方向的用地数blocksX2城市沿 X 方向的街区数blocksZ2城市沿 Z 方向的街区数curbHeight0.15人行道板抬升高度约标准路缘石外露高度设为 0 则不生成人行道板curbRadius5人行道板/路缘石的圆角半径默认参数下每个街区3×2 6个用地城市共2×2 4个街区即24 座塔楼每栋都有独立种子。3.1 layout 的推导公式cityLayout( parameters )源码第 150–163 行把上述参数收敛成一组派生尺寸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 };按默认参数计算blockW 90、blockD 60、cityW 2×90 1×22 202、cityD 2×60 1×22 142。注意cityW/cityD是「街区总宽 街区间夹缝街道」不含最外圈街道——这个边界语义在第 7 节做地面尺寸时要记住。layout对象会原样传给createBuildingMaterial(layout)和createRoadMaterial(layout)是建筑配色哈希与路面标线对齐的共同基准。4. build( materials )从网格到 24 座塔楼build( materials {} )源码第 45–122 行是整个类的核心流程如下。4.1 街区与用地的双层遍历外层双循环遍历街区内层双循环遍历用地const blockX - L.cityW / 2 bx * ( L.blockW L.street ); const blockZ - L.cityD / 2 bz * ( L.blockD L.street );城市以原点为中心排布街区步长为blockW street块宽 一条街道。每个街区循环开始时若curbHeight 0就为该街区记录一个位于其几何中心blockX blockW/2, 0, blockZ blockD/2的平移矩阵最后统一交给this.sidewalk.build( slabs )实例化——所以人行道是每个街区一块板而不是每个用地一块。4.2 每座塔楼的随机参数每个用地生成一座SkyscraperGenerator其参数全部由布局随机数驱动源码第 84–99 行参数取值公式设计意图源码注释seedMath.floor( random() * 100000 )每座塔楼独立种子totalHeight38 tall * tall * 114tall random()平方项让高度分布偏斜少量高塔、多数中低层footprint{ width: lot - 1 - random() * 4, depth: lot - 1 - random() * 4 }宽、深独立变化几乎占满整块用地邻楼贴得很近floorHeight3.4 random() * 1.8层高 3.4–5.2bayWidth1.9 random() * 2.1开间 1.9–4.0pierWidth0.4 random() * 0.5墙垛宽pierDepth0.3 random() * 0.4墙垛深chamferWidth转角用地3 random() * 4其余0斜切角只给四个「街角」用地setbackDepth40% 概率0.8 random() * 2否则0只有部分塔楼在顶部退台stringCourseEvery85% 概率3 floor( random() * 6 )否则0腰线层间隔 3–8 层其中转角斜切是一条有讲究的规则源码第 77–82 行注释斜切角只有正对着街区角落即街道交叉口时才读作「建筑语言」所以只有每个街区四角的地块被切角且切角朝向各自的街道交叉口其余地块保持方正。chamferCornerX/Z取-1/0/1三个值来指明切哪个角。塔楼构建完成后被放置在地块中心、抬升到路缘高度building.position.set( blockX ( lx 0.5 ) * L.lot, curb, blockZ ( lz 0.5 ) * L.lot ); building.castShadow building.receiveShadow true;注意y curb即curbHeight建筑底面正好落在人行道顶面不会陷入或悬空。所有生成器实例被收集进this.generators数组供后续统一释放。4.3 生命周期dispose 与重复构建三个要点都写在源码里build()第一行就调用this.dispose()源码第 47 行先清理上一次构建的几何体所以换种子重建是安全的直接改city.parameters.seed再build()即可dispose()第 124–133 行逐个释放this.generators中每座塔楼的几何体、释放人行道组并把this.group置空示例页面正是这样用的从场景中移除旧组 → 更新种子 → 重新build()并加入场景见第 7 节generateCity()。5. 塔楼材质工厂createBuildingMaterial文档说「Pass a building material to dress the towers」仓库同时导出了一个现成工厂 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即一份材质给所有塔楼上色颜色按地块网格单元哈希选取颜色板来自buildingPaletteSkyscraperGenerator.js 第 1177 行带陶土色点缀的砖石色系全部塔楼共用着色器把世界坐标positionWorld换算到「地块格点坐标」先除以blockW street/blockD street得街区序号再在块内除以lot得地块序号源码第 230–237 行用两组大素数73856093、19349663与种子做位异或生成cellKeyhash出一个稳定随机数从调色板里选色再乘以0.94 ~ 1.06的轻微亮度抖动让相邻同色楼略有差异由于一栋楼内取值恒定选色结果通过varying( buildingBase )在顶点阶段解析一次再插值最后交给createSkyscraperMaterial。这里也解释了第 3 节强调的「layout 是共同基准」材质哈希与build()的摆放都基于同一layout二者天然对齐。5.1 塔楼几何SkyscraperGenerator 的参数优先级每块用地上的塔楼由 SkyscraperGenerator 生成其build()的参数合成顺序值得注意源码第 274–276 行// precedence: fixed defaults seed-driven style caller parameters const p Object.assign( {}, SkyscraperGenerator.defaults, randomStyle( random ), this.parameters );固定默认值 种子驱动的风格随机 调用方显式参数三层依次覆盖。CityGenerator显式传入了高度、占地、层高等见第 4.2 节因此最终外观由这两层共同决定。固定默认值定义在 第 447–458 行seed: 35、totalHeight: 140、floorHeight: 4、bayWidth: 2.6、stringCourseEvery: 6、chamferWidth: 4、chamferCornerX: 1、chamferCornerZ: 1、setbackDepth: 1.5、acChance: 0.12窗户外机位的随机出现概率。从源码结构看该生成器把所有部件窗框、玻璃、腰线、墙垛、空调箱、檐口、女儿墙、尖饰、底层拱廊、退台板烘焙进单个非索引 BufferGeometry并为每个顶点写入partId属性PartId 定义WALL/PIER/FRAME/ORNAMENT/GLASS/AC使「一份材质分支渲染所有功能区」且整座楼只产生一次绘制调用floorHeight、bayWidth等尺寸还会被吸附到砖模数BRICK { height: 0.3, length: 0.6 }上第 48、278–285 行保证程序化砖缝与几何严格对齐。这部分实现细节超出 CityGenerator 文档范围但理解它有助于明白为什么 CityGenerator 只需传「尺寸类」参数就能得到风格统一又各不相同的天际线。6. 人行道与路缘石SidewalkGeneratorCityGenerator构造时创建的SidewalkGeneratorSidewalkGenerator.js有自己的默认值第 83–90 行SidewalkGenerator.defaults { width: 90, // 单块板覆盖的街区占地 depth: 60, height: 0.5, // 人行面高出路面的高度 radius: 5, // 圆角半径使人行道在路口转弯而非硬 90° curbWidth: 0.13, // 花岗岩路缘石顶宽约 5 英寸 curbLip: 0.01 // 路缘高出人行面的量近齐平 };其build( placements )接收 CityGenerator 传入的Matrix4[]每街区一个产出名为Sidewalk的Group内含两个 InstancedMesh行走面混凝土板内缩一圈、与路缘略微重叠以埋住接缝和全高路缘石环带外轮廓挖内孔挤出比行走面高出curbLip向下延伸到路面。两者材质分别由createSidewalkMaterial/createCurbMaterial程序化生成浇筑混凝土板块每板色调哈希 骨料斑点 双向切割伸缩缝与花岗岩路缘更暗更密、接缝约 1.5 m、面向路面的一侧更脏。与塔楼生成器「只产几何、材质外置」不同人行道生成器自己持有材质——源码注释明确解释了这一设计取舍第 18–19 行。7. 完整示例webgpu_generator_city.html 的组装方式仓库示例 examples/webgpu_generator_city.html 演示了 CityGenerator 的完整落地关键装配步骤依次为1渲染器与环境WebGPURenderer ACES 色调映射toneMappingExposure 0.45、阴影开启、Inspector调试面板、FirstPersonControls移动速度 15第一人称漫游2创建城市与地面——这一步正是「layout 对齐」的官方用法第 102–111 行city new CityGenerator( { seed: parameters.seed } ); // 路面尺寸 城市占地 四周各加一条街道宽人行横道宽度 const floorW city.layout.cityW 2 * city.layout.street; const floorD city.layout.cityD 2 * city.layout.street; const ground new THREE.Mesh( new THREE.PlaneGeometry( floorW, floorD ).rotateX( - Math.PI / 2 ), createRoadMaterial( city.layout ) ); ground.receiveShadow true; scene.add( ground );注意这里 2 * street的依据cityW/cityD不含最外圈街道第 3.1 节补上四周各一条街道后最外圈路面也能画下完整标线与人行横道。3共享建筑材质与首次构建materials { building: createBuildingMaterial( city.layout, parameters.seed ) }; generateCity();4换种子重建第 193–201 行展示了第 4.3 节的生命周期function generateCity() { if ( cityGroup ) scene.remove( cityGroup ); city.parameters.seed parameters.seed; cityGroup city.build( materials ); scene.add( cityGroup ); }Inspector 面板renderer.inspector.createParameters( City )提供seed0–100、timeOfDay6–18 点驱动SkyMesh太阳沿日弧移动并同步方向光与 PMREM 环境贴图、曝光与 Bloom 强度四个参数拖一下种子即可看到整座城市以确定性方式重新洗牌。7.1 道路材质createRoadMaterial 做了什么createRoadMaterial( layout )源码第 255–344 行返回一份MeshStandardNodeMaterial即「湿沥青 车道线 人行横道」全部世界空间程序化且严格对齐layout网格湿沥青底暖灰双色块状浇筑拼缝0x24262b/0x3b3f46按分形噪声混合、两尺度骨料颗粒、油污磨暗、发丝裂纹与低频水渍斑块水渍区粗糙度降到 0.32 以反射天空LOD 细节衰减detail smoothstep( 240, 25, p.distance( cameraPosition ) )——骨料、油渍、裂纹、磨损漆这些近景细节只在靠近相机时解析且噪声采样被包在If( detail 0 )分支里远端路面直接跳过节省片元算力标线对齐以blockW street/blockD street为周期取模step( blockW, fx )判定「是否在纵向街道内」从而只画街道、不画街区也不画进交叉口。每条街道画一条实线中央分隔线 每半边各一条虚线分隔线虚线周期 7每侧双向共四车道大陆式人行横道在每条街道臂靠近街区边缘 5 个单位处用周期 1.2、半宽 0.38 的横向条纹行进方向上拉长抗锯齿技巧lineAA/gridLine两个辅助函数第 202–218 行用fwidth计算坐标的像素足迹让细线宽度随视角缩放——注释称这样「thin road paint stays crisp and doesnt shimmer」细道路标线保持锐利、不闪烁程序化凹凸bumpNormal( height )第 187–198 行解释了为什么不用内建bumpMap——内建实现按 UV 偏移取高度对世界坐标键控的高度场梯度为零该函数改用屏幕空间导数dFdx/dFdy把高度梯度馈入 Mikkelsen 表面梯度法使 ~3 mm 骨料与 ~10 mm 裂纹真正扰动法线。8. 实践要点小结结合文档与源码使用 CityGenerator 时的实用清单确定性布局随机使用 mulberry32 PRNGcreateRandom第 166–179 行注释明确「a seed always lays out the same city」。种子同时作用于建筑摆放build内与塔楼内部风格每楼独立种子且createBuildingMaterial(layout, seed)的着色哈希也含种子因此换种子时应同步更新材质示例的generateCity只重建几何、复用初始材质——若需要颜色也随种子变化应重新调用createBuildingMaterial并替换materials.building布局对齐一切周边元素地面、标线、额外道具都应从city.layout取尺寸cityW/cityD不含最外圈街道加边时按 2 * street处理资源管理重建前build()会自动dispose()彻底移除城市时应调用city.dispose()释放全部塔楼几何与人行道材质边界塔楼只认一份材质按顶点partId分支着色所有功能区人行道材质由 SidewalkGenerator 内置自持路缘石开关由curbHeight 0控制设为 0 即整城落在路面上塔楼y也归零运行前提TSL 材质工厂需要three/webgputhree/tsl环境官方示例基于WebGPURenderer参考 文档页、主源码、SkyscraperGenerator、SidewalkGenerator 与示例页面可以继续深入。【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考