小明是个乐高迷,但他的搭法和别人不一样。别的小朋友一块一块地找零件、对照图纸,他却熬夜写了一本《乐高城市拼搭说明书》:里面规定了路要修多宽、街区分多大、高楼往哪儿扎堆、公园要留几块。第二天,他把说明书交给一群会读图的乐高小机器人,自己出门买早餐——等回来时,玄关地板上已经长出了一整座城市。更神奇的是,他改了一个数字再跑一遍,城市还是那座城市,但街道走向、楼群高低全都变了样。这就是“程序化建筑与城市生成”的魔力:程序员不亲手搭建每一栋楼、每一条街,而是设计一套“让内容自己长出来”的规则,再用代码把规则跑成一座完整、统一、可复现的虚拟城市。
掌握程序化建筑与城市生成,等于把“一个场景要磨三周”的工作流,变成“一个按钮生成一百座城”的自动化流水线。这是游戏、仿真与数字孪生领域内容生产的核心技能。
目录
可视化演示
展示程序化内容生成(PCG)三大核心:建筑生成(形状文法3规则R1 base/R2 floor/R3 roof+参数化PRM控制W/H/D生成5层楼房)、城市布局(7条道路网络3水平4垂直划分6个功能区块RES住宅/COM商业/IND工业每区块自动生成建筑)、PCG管线(4阶段NSE噪声→RUL文法规则→PRM参数控制→OUT网格输出+AI驱动增强自然度),三模式切换理解PCG。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+C1+'
基础概念:先写规则,再当建筑工人
小明发现,那本说明书威力再大,也离不开三样东西:一个起始数字、一套拼搭规则、一个最终成品。起始数字决定“第一块积木放在哪”,拼搭规则决定“下一块积木跟在哪”,最终成品就是整座城市。这三样东西,恰好就是程序化生成的骨架。
在计算机里,这三样东西分别叫“种子(Seed)”、“生成规则(Rule)”和“生成内容(Content)”。程序化内容生成(Procedural Content Generation, 简称 PCG)并不是“随机乱画”,而是“由规则驱动的自动化创作”。游戏里那些看似漫无边际的开放世界,背后往往只是一小段规则代码加上一个种子数字。种子一换,世界就换了副面孔;种子不变,世界可以在任何机器上原样复现。
建筑生成与城市生成,是 PCG 的两个层次。前者解决“一栋楼怎么长”,后者解决“一座城怎么排”。建筑生成关注墙、窗、屋顶的组合方式;城市生成关注路网、街区、地块的布局关系。两者的共同点,都是把“人的设计经验”翻译成“机器可执行的规则”。应用价值也很直接:人力只能手搓几十栋楼,规则驱动却能一口气生成几万栋,还能保证风格统一、永不重复。
有了这三样,小明再也不需要一砖一瓦地手搭。他只需要问自己一个问题:我想让这座城市长成什么样?然后把答案写成规则。这就好比给城市装上了一颗会思考的“性格种子”。
- 生成内容 C最终产出的城市、建筑或地形,是等式左边的“成品”。
- 生成器 G一个可执行的函数或程序,负责把“原料”加工成“成品”。
- 种子 s一个起始数字,相当于说明书上的“起始积木位置”,决定了所有后续选择。
- 规则集 R一组创作规则,如“路宽 10 米”“楼高 3 到 30 层”,约束生成风格。
核心算法:四本风格各异的拼搭说明书
同样的零件箱,不同的说明书,能拼出完全不同的东西。程序化生成也有四本经典“说明书”,分别擅长不同的活:形状文法负责“细胞分裂”,L 系统负责“树枝生长”,Perlin 噪声负责“大理石纹理”,波形函数坍缩负责“数独填格”。
形状文法(Shape Grammar)把一栋建筑看成一棵“分形树”:一个起点形状,按规则不断替换成更小的子形状。比如规则“楼体 → 楼体 + 一层楼层”,程序就会像细胞分裂一样,一层层把楼“长”高。这也是 Esri CityEngine 等专业工具的核心建模语言。L 系统(L-system)原是植物学家用来模拟植物生长的形式语言,如今被用来生成树冠、枝干,甚至教堂的飞扶壁。它的精髓是“递归替换”:一根枝上长两根小枝,小枝上再长两根小小枝,越分越细。
Perlin 噪声(Perlin Noise)负责“自然感”。纯随机数像撕碎的纸屑,杂乱无章;Perlin 噪声像大理石纹路,连续平滑、有高有低。它是地形高度、云朵密度、建筑高度起伏的通用“画笔”。波形函数坍缩(Wave Function Collapse, WFC)的思路来自量子力学:每个格子一开始处于“什么都有可能”的叠加态,随着相邻约束不断“坍缩”成确定形态——每块“瓦片”的四个边都必须和邻居严丝合缝,就像数独里每行每列不能重复。
如果把 L 系统比作“会不断分叉生长的树枝”,那么形状文法就是“按图纸细胞分裂的高楼”,Perlin 噪声是“自然起伏的地皮”,WFC 是“处处讲规矩的数独城市”。四本说明书一组合,城市就有了骨架、表情和秩序。
- 当前状态 μₙ第 n 代的结构字符串,比如一棵只有主干的小树。
- 规则集 P一组替换规则,如“主干 → 主干 + 左侧枝 + 右侧枝”。
- 应用替换把当前状态中所有可替换的符号,按规则各替换一次。
- 下一代状态 μₙ₊₁替换完全部符号后得到的新一代结构,可继续迭代。
城市布局:先画马路,再切地皮,最后盖楼
小明盖城的第一步永远是画马路,因为路是城市的骨架——路往哪儿伸,街区就往哪儿长。第二步是切地皮:被路围出来的一块块区域,就是未来的街区。第三步才是决定哪块地皮盖高楼、哪块留公园。
在城市布局里,“路网规划”是第一优先级。常见路网有三种形态:棋盘网格(曼哈顿式)、放射环线(巴黎式)、有机蔓延(欧洲老城式)。网格路网生成简单、寻路友好;放射路网突出市中心;有机路网依赖噪声与地形,更接近真实老街。路网画完,“地块划分”随之而来:把路围成的多边形切成可建造的单元,常用算法包括规则网格切分与 Voronoi 剖分。
地块有了,密度怎么定?城市不是均匀的立方体森林。现实规律是:越靠近市中心,地价越贵,楼盖得越高、越密;越往外,建筑越矮、越稀疏。这种“市中心高、郊区低”的分布,可以用指数衰减函数来刻画。此外,“功能区分布”——商业中心、住宅区、工业区、绿地——也会显著影响布局,程序可以按功能给不同区域配置不同的高度与风格参数。地标建筑则作为密度曲线上的“异常高值”被单独强调,成为城市的天际线焦点。
有了这张“密度地图”,小明的城市不再是呆板的方格阵——市中心高楼林立,越往外越像温柔的郊区。一座城市的气质,从确定密度函数那一刻起就已经注定了。
- 中心密度 ρ₀市中心(r=0)处的建筑密度,是所有密度的最大值。
- 距离 r地块到市中心的直线距离,越远密度越低。
- 衰减尺度 λ控制密度下降快慢的“软尺”,λ 越大,城市蔓延得越远。
- 指数衰减密度随距离指数下降,模拟“地价从中心向外跳水”的规律。
生成管线:一条从地皮到细节的流水线
搭一座城市不能“先盖 100 楼的第 50 层”。小明总结出一条流水线:先铺地基,再修路,再切地皮、盖楼,最后挂窗帘——每一步都站在上一步的肩膀上。
这条流水线在工程上叫“生成管线(Generation Pipeline)”。典型顺序是:地形生成 → 道路生成 → 地块划分 → 建筑生成 → 细节装饰。地形生成用 Perlin 噪声(还记得那块“大理石”吗?)叠出起伏的山丘与盆地,坡度太陡的地方没法修路;道路生成顺着低洼平缓处延伸,围出街块;地块划分把街块切成单元;建筑生成按密度与功能在单元上“长楼”;最后用窗户、路灯、植被等细节让城市活起来。
地形生成是这条管线“地基中的地基”,而它最常用的数学工具正是多层噪声叠加——把不同频率、不同振幅的噪声叠在一起:低频大振幅负责“大起大落”的山脉,高频小振幅负责“坑坑洼洼”的碎石。这样生成的地形既有大尺度轮廓,又有小尺度细节。关键还在于,每层噪声都由同一个种子驱动,所以只要拿住种子,整条管线就能在任何机器上复现同一座城。
流水线就像小明的“盖城总装车间”:地形是地基,道路是骨架,建筑是血肉,细节是妆容。而种子就是贯穿车间的主传送带——只要调整它,整条流水线就会为小明换出一座全新的城。
- 高度值 H(x,y)坐标 (x,y) 处最终的地形高度。
- 第 i 层噪声N 是基础 Perlin 噪声,输入坐标会被频率 fᵢ 缩放。
- 振幅 Aᵢ第 i 层噪声的权重,控制它对最终高度的贡献大小。
- 叠加求和把 k 层噪声按权重相加,层数越多,地形越有细节。
性能优化:让城市跑得动,而不是卡成 PPT
城市是长出来了,可小明的乐高小机器人一进市中心就卡成 PPT。他这才发现:生成一座城是一回事,让它“跑得动”是另一回事。
程序化生成的城市动辄几十万栋建筑,如果每栋都按最高精度渲染,任何显卡都会当场崩溃。第一个优化手段是 LOD(多细节层次):离得远的楼用几十个面的“粗积木”,离得近的用几千个面的“精密积木”。第二个是 GPU 实例化(Instancing):同款楼有 100 栋,没必要让 CPU 提交 100 次绘制命令——把 100 栋合成一次“批量倒模”,一次提交全部画完。
第三个是流式加载(Streaming):城市太大,没必要一口气全加载进内存,玩家走到哪就生成并加载哪一块,离开就卸载。第四个是空间哈希(Spatial Hashing):当程序需要查询“这栋楼旁边有什么”时,与其遍历全城,不如按网格把建筑分桶存放,直接查桶号。这四个手段组合起来,几十万栋楼也能在普通显卡上流畅运行。它们的核心收益可以用一个简单的公式理解:把渲染总耗时拆成不同精细度建筑的耗时之和,LOD 通过减少高精度建筑数量来压减总耗时。
小明的城市终于不再卡壳了——远处是憨憨的粗积木剪影,近处是精致的门窗细节,路过哪块就现场拼哪块。优化不是偷懒,而是把宝贵的算力花在玩家真正看得见的地方。
- 总耗时 T渲染一帧画面的总 GPU/CPU 时间,越小越流畅。
- 高精度部分近距离高模建筑数量 N_H 乘以每栋耗时 t_H,是开销大头。
- 中低精度部分中距与远距建筑分别对应 N_M、t_M 和 N_L、t_L,单体成本低。
- 近似求和总耗时约等于三档耗时之和,控制大头(高模数量)是关键。
实战应用:AI 建筑师登场,城市自己进化
到这一步,小明已经是个熟练的“规则设计师”。但他很快遇到新问题:他想做一座“有 1930 年代上海风情”的城,却说不清风情到底由哪些规则组成。这时候,AI 建筑师登场了。
AI 驱动的做法是:给算法大量真实城市照片或街区地图,让它自己学习“上海风格”背后的几何规律,再把这些规律变成生成参数或约束条件。现代工具已经开始用深度生成模型(如扩散模型、GAN)直接产出街景贴图或建筑体块,再用传统 PCG 规则去约束结构与布局。传统方法负责“讲规矩”,AI 负责“提供灵感”,两者正在融合。
在工程实践中,衡量 WFC 这类约束求解器状态有个常用指标——熵。熵越高,说明格子处于“什么都有可能”的不确定状态越多,冲突风险越大;求解器每一步都优先坍缩熵最低的格子,就像数独高手先填“可能性最少”的那一格。这套价值观在实战里同样重要:先做路网主干,再填街区;先锚定地标,再铺普通楼。
小明的“AI 建筑师”看完十万张老照片后,交出的生成参数让整座城瞬间有了旧时光的味道。传统规则负责说“不许这样”,AI 负责说“试试那样”。程序化城市,正在从“写死规则”走向“学会风格”。
- 概率 pᵢ某一格子选择第 i 种瓦片或建筑类型的概率。
- 信息量−log₂ pᵢ 表示“这个选择带来多少信息量”,概率越小信息越大。
- 期望求和对所有可能类型求和,得到该格子的平均不确定度。
- 取负号保证熵为非负值,熵越高越不确定,越低越接近确定答案。
AI 应用场景
场景一:开放世界游戏的城市自动生成
阶段:内容生成 / 关卡搭建
解决的问题:手搓一座 5 公里见方的现代都市,传统美术团队要数月;PCG 用路网、密度函数和 Shape Grammar 在几小时内生成,再叠加 AI 后处理去重、补细节。
具体案例:某开放世界项目先用 Perlin 噪声生成地形,再用网格路网切街区,最后用 Shape Grammar 批量长出写字楼与住宅。QA 只需检查路网是否连通、建筑是否穿模,而不是一栋栋地审。
场景二:智慧城市数字孪生
阶段:仿真数据准备
解决的问题:城市交通、排水仿真需要“足够像真的”的街区网络,真实数据又涉及隐私。PCG 可以按真实城市的统计规律生成同等规模的“替身”城市。
具体案例:研究团队以目标城市的路网密度、人口热力分布作为密度函数 ρ(r) 的参数,生成一万个虚拟街区做交通流仿真,评估“地铁改线后早高峰会怎样”。
场景三:影视与动画的背景城市
阶段:美术资产管线
解决的问题:电影需要一座“未来感但又不认识的都市”,纯手搭成本太高,纯随机又不可控。
具体案例:概念团队以手工设计的少量建筑“瓦片”为原料,用 WFC 自动铺满整个远景,再让 AI 生成贴图并做风格迁移,只保留近处几栋手工精修楼。
Python 演示
import random class CityGenerator: """用种子驱动的程序化城市生成器, 对应文章中的生成管线""" def __init__(self, seed=42): self.seed = seed self.rng = random.Random(seed) self.size_x = 60 self.size_y = 40 self.grid_map = [[0 for _ in range(self.size_x)] for _ in range(self.size_y)] self.buildings_list = [] def generate(self): self._paint_water() self._lay_roads() self._sow_parks() self._rise_buildings() return self.grid_map def _paint_water(self): # 在右下角画一个圆形湖泊, 模拟地形约束 for y in range(self.size_y): for x in range(self.size_x): d = ((x - 48) ** 2 + (y - 6) ** 2) ** 0.5 if d < 9: self.grid_map[y][x] = -1 def _lay_roads(self): # 每隔 6 格画一条南北向主干道 for i in range(0, self.size_x, 6): for y in range(self.size_y): if self.grid_map[y][i] == 0: self.grid_map[y][i] = 1 # 每隔 6 格画一条东西向主干道 for i in range(0, self.size_y, 6): for x in range(self.size_x): if self.grid_map[i][x] == 0: self.grid_map[i][x] = 1 def _sow_parks(self): # 剩余空地上以 6% 概率生成公园, 对应功能区分布 for y in range(self.size_y): for x in range(self.size_x): if self.grid_map[y][x] == 0 and self.rng.random() < 0.06: self.grid_map[y][x] = 2 def _rise_buildings(self): # 按概率区间生成低层/高层/地标, 对应建筑生成 for y in range(self.size_y): for x in range(self.size_x): if self.grid_map[y][x] == 0: v = self.rng.random() if v < 0.55: self.grid_map[y][x] = 3 elif v < 0.82: self.grid_map[y][x] = 4 else: self.grid_map[y][x] = 5 self.buildings_list.append((x, y)) def render(self): symbols = {-1: '~~', 0: '..', 1: '==', 2: '##', 3: '[]', 4: 'HH', 5: 'TT'} lines = [] for row in self.grid_map: lines.append(''.join(symbols[c] for c in row)) return '\n'.join(lines) if __name__ == '__main__': city = CityGenerator(seed=2024) city.generate() count = sum(1 for row in city.grid_map for c in row if c >= 3) print('生成建筑地块数: %d' % count) print(city.render())
self.rng = random.Random(seed)用固定种子创建可复现的随机源,同一颗种子永远长出同一座城。self.grid_map = [[0 for _ in range(self.size_x)] for _ in range(self.size_y)]一张二维地图,0 代表未开发空地。d = ((x - 48) ** 2 + (y - 6) ** 2) ** 0.5计算地块到“湖心”的距离,模拟地形约束。if d < 9: self.grid_map[y][x] = -1距离小于 9 的格子变成水域,道路会绕开它。for i in range(0, self.size_x, 6):每隔 6 格画一条南北向主干道。if self.grid_map[y][i] == 0: self.grid_map[y][i] = 1只在空地上铺路,避免马路穿湖。if self.grid_map[y][x] == 0 and self.rng.random() < 0.06:空地上以 6% 概率长出公园。v = self.rng.random()每块空地掷一次骰子,按区间分配建筑类型。symbols = {-1: '~~', 0: '..', 1: '==', 2: '##', 3: '[]', 4: 'HH', 5: 'TT'}把数字地图翻译成一眼能看懂的字符图例。if __name__ == '__main__':只有直接运行时才生成并打印,被 import 时不触发。
常见误区
误区程序化生成不就是随机扔骰子吗?反正结果都是碰运气,多跑几次总能跑出一座好城市。
正解程序化生成的核心是“规则 + 种子”:随机只是规则允许范围内的随机,种子保证结果可复现。就像乐高说明书:虽然每步都有几种选择,但选择范围和顺序全是写死的。换种子只是换了“选择答案的起点”,并没有换一本说明书。
为何常见:初学者看到每次结果不同,误以为本质是随机;实际上“可复现的随机”和“听天由命的随机”是两回事。
误区机器能自动生成城市,美术师是不是要失业了?以后只要调调参数就行了吧。
正解PCG 自动化的是“组合与布局”,而建筑贴图、基础模型、风格设定仍然需要美术手工制作——它们就是乐高零件箱里的“积木”。没有好零件,规则再强也只能拼出千篇一律的方块。地标建筑、剧情关键场景也往往要手工精修,PCG 之后再叠加人工打磨。
为何常见:过度神化“全自动”概念,忽略了任何生成系统都建立在资产与规则的质量之上。
误区写好算法点一下按钮,整座城市就该瞬间出现,这才配叫程序化生成。
正解真实工程里城市是“管线”逐步产出的:地形→道路→地块→建筑→细节,每一步都可能失败或需要回炉。复杂场景生成要数秒到数分钟,而且普遍采用流式加载、分块生成,而不是一次性算完。演示视频里的“瞬间长成”其实是快进效果。
为何常见:把精心剪辑的演示视频当成了真实耗时。
误区WFC 要反复试错找冲突,规模一大就卡死,根本没法用于整座城市。
正解WFC 的瓶颈可以通过三类手段控制:优先坍缩低熵格子减少回溯、把瓦片组合预计算成查表、增量式更新只重算受影响区域。配合分块生成,WFC 完全可以用于大规模关卡与城市街区。
为何常见:只盯着朴素实现的最坏情况,没了解约束传播和工程优化手段。
面试题
Q1. 在程序化生成中,种子(seed)最主要的用途是什么?
- A.决定生成的唯一确定性起点← 正确答案
- B.加快计算机的运算速度
- C.提高画面的分辨率质量
- D.替代所有美术资源文件
答案:A。种子是生成流程的“唯一确定性起点”:同一算法加同一种子必然得到同一结果,换种子得到新变体。就像换一块乐高起始积木,并不会让说明书跑得更快或画质更好。B、C、D 分别混淆了性能、画质与资产替代,都不是种子的职责。
Q2. L系统(L-system)最擅长生成下列哪类结构?
- A.递归生长的分支结构← 正确答案
- B.完全等距的网格道路
- C.均匀分布的单色纹理
- D.固定不变的手工模型
答案:A。L 系统通过替换规则递归生成分支结构,天生适合树冠、枝干、藤蔓乃至哥特式飞扶壁。网格道路是布局算法的任务,单色纹理通常用噪声,手工模型则与程序化生成相对。选项 A 正是“枝上生枝”的那本说明书。
Q3. Perlin噪声相比纯随机数的关键优势是什么?
- A.数值连续平滑且自然← 正确答案
- B.数值完全不可预测
- C.计算速度一定更快
- D.不需要任何外部种子
答案:A。Perlin 噪声的核心价值是“连续平滑”:相邻位置的取值变化自然,适合模拟地形起伏。B 描述的是随机数,C 的速度优势并不绝对,D 说反了——噪声同样依赖种子。就像大理石纹理,平滑连续才像自然,纸屑般乱跳只能是噪声。
Q4. 波形函数坍缩(WFC)算法的核心思想更接近什么?
- A.用约束传播解决冲突← 正确答案
- B.用暴力枚举穷举所有
- C.用随机数独立决定格
- D.用固定模板复制粘贴
答案:A。WFC 的精髓是“约束传播加低熵优先”:每个格子先处于多种可能,再按邻居约束逐步坍缩,像数独高手先填可能性最少的格。暴力枚举、独立随机、固定模板都没有利用邻接约束,因此都不是它的核心思想。
Q5. 程序化城市生成管线中,第一步通常是?
- A.生成地形起伏基础← 正确答案
- B.直接放置高楼大厦
- C.绘制室内家具装饰
- D.编写完整的剧情线
答案:A。生成管线的顺序是“地形→道路→地块→建筑→细节”。地形是地基,道路要顺着平缓地形走,建筑要落在地块里。先放高楼、先摆家具、先写剧情都违背了这条依赖链。就像搭乐高城市,先有底板,才有马路和楼房。
Q6. 提升城市场景渲染性能的常用手段包括?
- A.远处建筑使用低模← 正确答案
- B.相同模型批量绘制← 正确答案
- C.边游玩边流式加载← 正确答案
- D.加载全部高清贴图
答案:ABC。LOD(远处低模)、GPU 实例化(同款批量绘制)、流式加载(边走边加载)都是标准优化手段。D 是反面选项:一次性加载全部高清贴图会瞬间耗尽显存,正是优化要避免的做法。
Q7. Shape Grammar中“替换规则”的含义是什么?
- A.用子形状替换父形状← 正确答案
- B.直接删除所有形状
- C.随机改变颜色数值
- D.把二维投影成三维
答案:A。Shape Grammar 通过“用子形状替换父形状”不断细化结构,就像细胞分裂:楼体替换成墙加屋顶,墙再替换成一排砖。删除形状、改颜色、投影变换都与“细化结构”无关。
Q8. 影响城市布局密度分布的因素包括?
- A.到城市中心的距离← 正确答案
- B.道路网络的连通性← 正确答案
- C.地形起伏的限制← 正确答案
- D.渲染引擎的版本号
答案:ABC。中心距离决定密度衰减(ρ(r)公式),道路连通性决定地块的可达性,地形坡度决定哪里能盖房。渲染引擎版本只影响呈现效率,不改变城市布局的几何规律。
Q9. 同样的算法和种子,生成结果通常?
- A.完全一致可复现← 正确答案
- B.每次都完全不同
- C.仅颜色发生变化
- D.结果完全无法预测
答案:A。种子加规则确定后,生成结果是完全确定、可复现的。这正是 PCG 可调试、可验收的基石——如果每次都不同,QA 将无从下手。C 和 D 都否定了确定性,B 则是把“换种子”和“同种子”混为一谈。
Q10. LOD(多细节层次)技术的主要做法是什么?
- A.按距离切换模型精度← 正确答案
- B.提高贴图的饱和度
- C.降低游戏的运行帧率
- D.增加场景的灯光数量
答案:A。LOD 按物体与摄像机的距离切换精度:近处用高模、远处用低模,从而压减公式 T 中最重的 N_H·t_H 项。提高饱和度、降低帧率、增加灯光都与“多细节层次”无关,反而可能拖慢性能。