一块样板瓷砖,铺满整面墙。想象你正给浴室的瓷砖墙换新:手里只有一小块样品,上面是紫、红、绿三种颜色的几何图案。你希望整面墙连起来时,花纹的“节奏”和样品一样自然,却又不想单调地复制粘贴同一块。普通人拿起砖就贴,结果总在贴到一半时发现两块砖的花纹对不上,只好撬下来返工。 聪明的老师傅完全换了个思路:他先对着样品算出“哪种砖能和哪种砖挨着”,然后在墙上摊开一张“可能性地图”——每个位置都记着“这里还有几种砖可能合适”。接下来他永远只做两件事:先贴那个“可能性最少”的位置,贴完立刻回头检查邻居,把不可能的花色划掉。就这么一遍遍推下去,整面墙顺理成章铺完,几乎不用返工。这套思路,就是 Wave Function Collapse(波函数坍缩,WFC)——当前程序化内容生成领域最热门的算法之一。
掌握 WFC,你就掌握了“从一小块样例自动铺出整个世界”的能力。它是游戏地形、关卡、建筑自动生成的核心工具箱,也是 AI 从业者做内容自动化时性价比极高的技能。
可视化演示
展示WFC程序化生成算法的网格坍缩过程,包括熵计算、约束传播与回溯机制,交互调节网格大小查看生成效果。
本节配有交互动效(进入视口后自动加载,约 3KB)。
WFC网格坍缩过程 熵最小优先:选择可能性最少的格子进行坍缩 约束传播:坍缩后移除邻居不兼容的瓦片选项 Collapse(cell) = observe(min_entropy_cell) then propagate WFC模仿量子力学波函数坍缩,通过约束传播实现自洽的程序化生成
基本概念
铺设浴室的匠心,全在理解那一小块样品。老师傅拎着样品站到墙前,第一件事不是动手,而是把样品翻来覆去地看:哪些花纹总是接在哪些花纹旁边,哪些组合从不会出现。这段观察,就是 WFC 的“训练阶段”。训练产物就叫瓦片——在像素世界里,一个瓦片是 N×N 的一小块图案;在游戏关卡里,一格草地、一堵墙、一段走廊,都可以是一枚瓦片。
观察完毕,老师傅把整面墙想象成一张棋盘,每个格子都先标记“这里可能贴 a、b、c 三种砖”。在 WFC 里,这种“各有可能”的中间状态借用量子力学叫做波函数——就像薛定谔的猫在被观察之前既是活的又是死的,一个网格位置在被确定之前,可以同时是草地、墙和走廊。而“这个位置最终只能选一种”的确定过程,就叫坍缩。
瓦片模型的思想是:给定一个由小图块组成的样本,程序统计其中“相邻关系”,再把这种关系推广到大网格。每个位置维护一个候选集合,候选在规则作用下逐渐缩小,直到每个位置只剩一种可能。
所以你看,所谓“波函数”并没有玄学——它不过是老师傅墙上那张可能性地图里的一行批注:这个位置还能不能贴、还能贴哪几种砖。理解了瓦片与候选集合,就拿到了读懂整篇 WFC 的钥匙。
- 位置 p标记墙面上的坐标点,比如第 3 行第 5 列。
- 候选集合 C(p)所有还没被规则排除的瓦片组成的一个数学集合。
- 候选瓦片 t集合里的每个元素都来自训练样本拆出的瓦片库。
- 集合大小 kk=1 表示已经贴死了;k=0 表示矛盾;k>1 则是尚未决定的叠态。
算法原理
确立了“候选集合”之后,老师傅面对的难题是:这么多格子,先贴哪一块?外行随手抓起最左边那块砖就铺,老师傅却眯着眼扫一圈墙,专挑“只剩下一种可能”的位置下手——因为那里最不容易出错。这就是 WFC 最令人眼前一亮的规则:永远选择熵最小的格子。
在信息论里,熵衡量不确定性:候选越多,格子越“没准”。WFC 用香农熵给每个格子打分,专门挑分数最低的格子下刀。选好位置后,从它的候选集里按权重随机抽一个瓦片,这就是坍缩。
坍缩不是结束。邻居格子立刻被重新审视,凡是跟新瓦片边缘对不上的候选全部划掉;邻居们的变化又继续向外传递,像水波一样一层层推开,直到整个网格不再变化——这一步叫约束传播。若某个格子的候选集合被划空,说明之前某次选择埋下了雷,就要回溯:退回到历史分叉点,换一个选择重新推进。
先动最确定的地方,再让确定性自己长大——这就是老师傅“越贴越轻松”的秘密,也是 WFC 用几十行代码就能复现的施工智慧:不确定性越小的地方先出手,之后只需要等着约束把答案送上门。
- 熵值 H(C)格子候选集合 C 的不确定性度量,数值越小越确定。
- 对所有候选求和把集合里每种可能瓦片的贡献累加在一起。
- 候选概率 p_i第 i 种瓦片在这个位置出现的概率,通常由样本频率决定。
- 对概率取对数概率越接近 1,这一项越接近 0,整体熵越低。
关键流程
把老师傅一天的活儿拆成工序,恰好是 WFC 的三步流水线:第一步“瞧样品”,第二步“贴砖”,第三步“验收”。瞧样品的产出有两样东西:瓦片库和统计表。统计表记录每种瓦片在样本里出现了多少次——出现多的砖说明它更“搭调”,生成时应该被更优先抽中。
第二步是迭代生成:每坍缩一个格子,都按统计频率做一次加权随机;传播环节则靠预生成的“邻接表”快速判断“哪种砖能挨着哪种砖”。这两件事反复交替,直到所有格子都只有一种候选。第三步是结果校验:检查每条相邻边是否匹配、统计整体风格是否贴近样本,不合格就换种子重跑或触发回溯。
频率转成权重,是生成环节最核心的一步。它决定了随机往哪里偏:样本里常见的瓦片,在输出里也更容易出现,从而保住“神韵”。
工序清晰,返工就少。老师傅从来不是凭感觉贴砖,而是拿着一张“频率账本”、一张“邻接清单”,按顺序稳稳走完——这正是 WFC 工程实现的骨架:分析样本、提取模式、迭代生成、校验结果。
- 分子 f_t瓦片 t 在样本中出现的次数,是频率的原始计数。
- 分母汇总把样本里所有瓦片的出现次数加起来,得到总数。
- 比值即权重用 t 的计数除以总数,得到一个 0 到 1 之间的概率。
性能特性
墙越铺越大,老师傅发现体力吃紧。WFC 同样有一本“性能账”:代价主要花在三处——格子数量、候选瓦片数量,以及每次坍缩后约束“涟漪”能波及多广。三者共同决定了生成一堵墙要花多久。
粗略地说,一次完整生成的时间复杂度约为 O(N·M·K):N 是格子总数,M 是瓦片种类数,K 是约束传播平均扫过的格子数。样本越大、瓦片切得越细,M 和 K 就越大,生成就越慢。内存方面,每个格子都要维护候选列表,M 很大时列表开销可观,工程上常用位掩码优化——一个整数代表一整组候选瓦片。
性能之外还有一本更隐蔽的账:失败概率。输出规模越大,中途出现矛盾的概率会指数上升。因此成熟项目很少一把生成超大网格,而是分块生成,再在接缝处小心缝合。
聪明的老师傅从不肯一口气铺完整面 10 米的墙,他会按砖缝把墙分成几段,每段单独拿下,再接缝处小心对齐——这就是工程界的“分块生成 + 边界缝合”。理解了这本账,你就明白为什么 WFC 项目都要优先讨论规模,而不是炫技。
- 网格规模 N所有需要填充的格子总数,10×10 的网格 N=100。
- 瓦片种类 M样本拆出的瓦片类型总数,决定候选集合的平均宽度。
- 传播范围 K每次坍缩后,约束平均波及的格子数量。
- 量级记号 O表示“大致正比于”,用于描述随规模的增速。
优化策略
同样是贴砖,老师傅的抽屉里有不少“小机关”:先是把样品里的频率做成一张查表,再给每种候选配一个“适配分”,综合起来决定先贴谁。这些机关放在 WFC 里,就是一套优化策略:不改变核心算法,但让生成更快、更稳、更可控。
常见的优化有四类。启发式选格:不只数候选个数,还参考频率与边界信息,给“更危险”的位置更高优先级;模式预计算:把邻接兼容关系提前算好存成表,传播时直接查表,省去现场判断;动态权重:生成中途根据局部密度微调频率权重,防止输出过于散乱;并行加速:开多个候补方案同时试错,谁先成功用谁。
加权评分是启发式的常用骨架:为每种候选计算一个分数 s,它是频率项与局部相容项的加权和。α 控制“风格跟随程度”,β 控制“结构合法优先度”——α 调大更像样本,β 调大更少矛盾。
老师傅的“小机关”看似琐碎,却让整面墙既保留样品的神韵,又几乎不返工。优化策略的本质,就是把“手感”变成可调的旋钮——这正是 WFC 从玩具走向工业级产品的关键一步。
- 综合评分 s(t)瓦片 t 在候选中的综合优先级,分数越高越倾向被选中。
- 频率项α 乘以样本频率权重,保留样本的统计风格。
- 相容项β 乘以局部相容度,比如与已确定邻居的匹配边数。
- 两个系数α 与 β 是人为设定的超参数,二者比例决定优化偏向。
实际应用
墙面铺完,老师傅的名声传到了游戏圈。今天的游戏开发者发现:把“瓷砖”换成“草地、河流、围墙”,把“砖缝对齐”换成“地形语义匹配”,WFC 就能自动铺出一整个地图。它不再只是装修工具,而是程序化内容生成的通用引擎。
《塞尔达传说:旷野之息》的开发团队曾分享过地牢等内容的程序化设计经验;独立游戏 Townscaper、Bad North 也常见 WFC 的身影。在地形生成里,水、草地、山丘被抽象成瓦片,边缘约束保证湖泊不会突兀地爬上山;在建筑布局里,房间是瓦片,门是边缘标签,算法自动排出不穿墙的户型;在纹理创作里,一小幅像素画瞬间扩展成整面墙纸。
落地时,团队会先定义验收指标:十次生成里成功几次。只有成功率达标,管线才敢接进正式流程,否则玩家会频繁遇到“死胡同地图”或“断头路”。
从浴室的瓷砖到游戏的地牢,铺下去的始终是同一个道理:定义好“什么能挨着什么”,剩下交给约束去自然生长。WFC 的真正价值,是让“随机”变成了“有品味的随机”。
- 成功次数在 N_ok 次生成中,输出完全合法、通过校验的次数。
- 失败次数因为矛盾或校验不过而被丢弃的次数。
- 成功概率两者之比,1 表示次次成功,0 表示次次失败。
工程实践
老师傅退休前,把毕生经验写成了一本施工手册。WFC 的工程落地,同样需要这样一本“配置手册”——因为算法只负责执行,而质量、风格、成功率全由参数决定。
手册里最重要的几个旋钮:样本尺寸(决定风格来源的丰富度)、瓦片尺寸 N(N 越大输出越死板,N 越小越自由)、随机种子(决定结果能否复现)、回溯上限(兜底策略,超过就放弃重来)、α/β 权重(美观与合法的取舍)。调参建议顺序:先固定瓦片尺寸,再换种子跑几遍看方差,最后微调权重。
调试时要善用可视化:把每个格子的候选数画成热力图,一眼看出哪里“堵住了”;出现矛盾时回溯到最近分支换选择。配套的质量评估指标是局部模式匹配率——统计输出中与样本一致的局部小块占比,量化“像不像”。主流的开源实现(如多种语言的 wfc 库、Unity 插件)都内置了这些调试工具。
手册最后一页写着:WFC 不替你思考,“什么能挨着什么”的定义权永远在你手里。理解了这句话,你才算真正出师——工具只是约束的放大器,设计师才是规则的定义者。
- 匹配数量输出里与样本中某个局部模式完全一致的块数。
- 总块数输出网格中所有可统计的局部小块总数。
- 匹配率 Q两者比值,越接近 1 说明风格还原度越高。
AI 应用场景
游戏地牢与关卡自动生成
阶段:程序化内容生成(关卡布局)
解决的问题:手工设计地牢关卡成本高、重复度高;传统随机生成又常产出死胡同或不可达区域。WFC 把房间、走廊、门抽象成瓦片,用邻接约束保证连通性与可达性,让“随机”与“合法”兼得。
具体案例:某独立游戏团队用 WFC 生成 10×10 的地牢地图:样本是一张手工绘制的 5×5 小地图,程序学习房间与走廊的相邻规律后,批量产出数百张风格一致的新地图,再由策划挑选微调。
像素纹理与美术资产扩展
阶段:资产生成与增强
解决的问题:美术画一小块背景或纹理后,要手动铺满大场景,既费时又容易露馅。WFC 的叠加(overlapping)模型可以直接把像素画当作样本,自动生成风格统一的大尺寸纹理。
具体案例:独立游戏团队把 16×16 的草地像素块喂给 WFC,生成 256×256 的整张地表贴图,边缘过渡自然,极大缩短了美术出图周期。
建筑布局与城市预演
阶段:方案快速原型
解决的问题:建筑方案初期需要快速试排房间,传统办法靠人工反复调整。WFC 将房间类型作为瓦片、门与走廊作为边缘约束,几分钟内给出多套可行布局,供设计师挑选。
具体案例:某建筑科技公司用 WFC 生成公寓户型草图:输入“客厅、卧室、厨卫”的邻接偏好,算法输出几十种不穿墙、有采光的候选方案,作为人工深化设计的起点。
Python 演示
import random # 瓦片库:每个瓦片用四向颜色描述,0=上 1=右 2=下 3=左# 颜色: 0=紫 1=红 2=绿TILES = [ (0, 0, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 1, 1, 0),] def build_allowed(): allowed = [dict() for _ in TILES] for a in range(len(TILES)): for d in range(4): allowed[a][d] = [ b for b in range(len(TILES)) if TILES[a][d] == TILES[b][(d + 2) % 4] ] return allowed ALLOWED = build_allowed() class WFC: def __init__(self, rows, cols, seed=0): self.rows = rows self.cols = cols self.rng = random.Random(seed) # 每个格子维护候选瓦片列表,初始时全部可能 self.cell_options = [ [list(range(len(TILES))) for _ in range(cols)] for _ in range(rows) ] def propagate(self): changed = True while changed: changed = False for i in range(self.rows): for j in range(self.cols): if len(self.cell_options[i][j]) == 1: continue # 四个方向: 右 下 左 上 for d, (di, dj) in enumerate(((0, 1), (1, 0), (0, -1), (-1, 0))): ni, nj = i + di, j + dj if not (0 <= ni < self.rows and 0 <= nj < self.cols): continue other = self.cell_options[ni][nj] keep = [ a for a in self.cell_options[i][j] if any(b in ALLOWED[a][d] for b in other) ] if len(keep) != len(self.cell_options[i][j]): self.cell_options[i][j] = keep changed = True if not keep: return False return True def solve(self): while True: best = None for i in range(self.rows): for j in range(self.cols): n = len(self.cell_options[i][j]) if n > 1 and ( best is None or n < len(self.cell_options[best[0]][best[1]]) ): best = (i, j) if best is None: return True i, j = best # 坍缩: 从候选中随机选一个固定下来 self.cell_options[i][j] = [self.rng.choice(self.cell_options[i][j])] if not self.propagate(): return False def result(self): return [ [self.cell_options[i][j][0] for j in range(self.cols)] for i in range(self.rows) ] def main(): wfc = WFC(10, 12, seed=42) if not wfc.solve(): print("出现矛盾,请尝试换种子或调整瓦片库") return for row in wfc.result(): print(" ".join(str(t) for t in row)) if __name__ == "__main__": main()
TILES = [...四个方向的颜色...]定义瓦片库;每个元组描述一块砖的四边颜色。allowed[a][d] = [b for b in ... if ...]预计算邻接兼容表:瓦片 a 的 d 方向能接哪些瓦片。self.cell_options = [[...]]波函数叠态:每个格子维护一份候选瓦片列表。n > 1 and (best is None or n < len(...))最小熵选格:优先处理候选最少的格子。self.cell_options[i][j] = [self.rng.choice(...)]坍缩操作:把候选列表缩成一个随机选择。if any(b in ALLOWED[a][d] for b in other)约束传播核心:判断邻居候选里是否存在与自己兼容的瓦片。if not keep: return False矛盾检测:候选被清空说明此路不通。if not self.propagate(): return False主循环兜底:矛盾即失败,可换种子重试。if __name__ == "__main__": main()可执行入口,直接运行即可看到生成的瓦片网格。
常见误区
误区WFC 的输出每次都不同,感觉跟随机填充没什么区别,想要什么效果全靠碰运气,没法用于严肃的项目。
正解WFC 的随机只发生在“从候选集中挑一个瓦片”这一步,整体结构始终被样本学到的邻接约束牢牢锁住。它更像“有品味的随机”:局部看有随机性,全局看风格稳定。工程上还可以用固定种子复现结果,用权重引导风格,用 α/β 调平衡,可控性远高于纯随机。
为何常见:因为演示里每次点击结果都不一样,初学者容易把“随机性”误解为“不可控”。
误区训练样本越大,学到的东西越多,输出质量就越高,所以应该拼命加大样本数据。
正解样本过大会引入大量模式,每个格子的候选变多、熵变大,矛盾概率随之上升,输出也可能变得“散”而失去风格。相反,小而精的样本配合恰当的瓦片尺寸,更容易生成统一、可预测的风格。样本是风格的“种子”,不是数据越多越好。
为何常见:直觉上“数据多 = 质量好”在机器学习里常见,但 WFC 是约束满足问题,样本复杂度与可控性之间存在冲突。
误区跑完 WFC 就一定能得到合法输出,如果失败了就是代码写错了。
正解WFC 本身不保证成功——在无回溯的版本里,某个格子候选集被清空就意味着矛盾;在回溯版本里,超出回溯上限也会失败。失败是常态,工程上要靠换种子、调约束、改权重来降低失败率,并用成功率作为验收指标。
为何常见:教程和演示视频往往只展示成功案例,让人误以为算法天然“不会出错”。
误区WFC 就是给像素小游戏铺地砖的,3D 场景、音频、剧情这些复杂内容跟它没关系。
正解WFC 的核心是“带边缘标签的对象 + 邻接约束”,瓦片可以是任意概念:3D 体素块、房间功能、音乐小节、任务事件。只要你能定义“什么能挨着什么”,WFC 就能生成对应结构。它本质是通用的约束满足生成器,2D 像素只是最常见的演示形式。
为何常见:因为绝大多数入门教程都用 2D 像素样例演示,概念被演示形式限制了。
面试题
Q1. WFC 最核心的目标是什么?
- A.随机铺满所有格子
- B.从样本学局部模式并扩展← 正确答案
- C.用神经网络生成整幅图
- D.把样本逐块复制粘贴
答案:B。老师傅从不凭空贴砖,而是先观察样品确定局部搭配,再把这些搭配推广到整面墙。A 是无脑随机,谈不上“学习”;C 用神经网络违背 WFC 的思想;D 只是复制,缺少生成新结构的能力。B 正是 WFC 的本质。
Q2. WFC 中“熵”形容的是什么?
- A.生成一帧画面的耗时
- B.格子候选瓦片的剩余量← 正确答案
- C.输出图片文件的大小
- D.样本图案颜色的数量
答案:B。熵衡量不确定性,对应一个格子“还有多少种瓦片可能”。候选越少熵越低,越确定,越应该先处理。耗时、文件大小、颜色数量都不是熵在 WFC 里的含义。
Q3. 每轮迭代优先选择哪个格子?
- A.网格左上角第一个格子
- B.候选瓦片最多的格子
- C.候选瓦片最少的格子← 正确答案
- D.随机抽取一个未定格子
答案:C。老师傅专挑“可能性最少”的位置下手,因为那里最不容易贴错。候选最少的格子熵最低、风险最小,优先处理能让后续传播更稳定。候选最多的格子反而最不确定,应留到最后。
Q4. 约束传播做完后的效果是什么?
- A.邻居自动生成最终图案
- B.邻居同步排除不可能候选← 正确答案
- C.网格直接渲染成图片
- D.样本被重新切分成瓦片
答案:B。坍缩一块砖后,邻居要划掉与新砖边缘对不上的候选,这个“划掉”的动作逐层外传,就是约束传播。它不会替邻居生成答案,也不负责渲染或拆样本,只是让所有格子的候选信息保持一致。
Q5. 瓦片间能否相邻的规则来源于?
- A.运行时随机生成规则
- B.统计样本边缘的颜色← 正确答案
- C.由用户逐条手写规则
- D.从最终输出反推规则
答案:B。WFC 的邻接规则从样本中统计得来:两块瓦片的接触边颜色相同,就认为它们可以相邻。规则不是随机来的,也通常不必逐条手写——这正是 WFC 比传统手工规则引擎省力的原因。
Q6. 某格子候选集合变为空时,通常意味着?
- A.生成已经圆满成功
- B.之前某次选择导致矛盾← 正确答案
- C.样本太小需要重采样
- D.输出尺寸设置过大
答案:B。候选清空说明没有瓦片能满足所有邻居的约束,等于把路走死了,即“矛盾”。此时要么回溯换选择,要么换种子重来。这与成功、样本大小、输出尺寸都不是直接因果关系。
Q7. 与瓦片模型相比,重叠(overlapping)模型的特点是?
- A.无需任何训练样本输入
- B.提取 N×N 像素块作为模式← 正确答案
- C.生成速度一定更快
- D.只能用于 2D 像素画
答案:B。重叠模型把样本切成 N×N 的像素块作为模式,能直接处理像素级输入,无需人工设计瓦片。它仍然需要样本,速度不一定更快,也不局限于像素画。这正是它适合纹理扩展场景的原因。
Q8. 下列哪些因素会显著影响 WFC 性能?
- A.网格格子的总数量← 正确答案
- B.候选瓦片的种类数← 正确答案
- C.约束传播影响范围← 正确答案
- D.输出图片的色彩格式
答案:ABC。时间复杂度 T(N)=O(N·M·K) 表明,格子数、瓦片种类数、传播范围三个因素共同决定耗时。色彩格式是渲染层的问题,与算法本身的求解开销无关。
Q9. 降低生成矛盾概率的有效手段有哪些?
- A.增加更大更全的样本
- B.调制匹配需要的邻接约束← 正确答案
- C.允许回溯重新选择← 正确答案
- D.去掉所有传播过程
答案:BC。更大的样本可能引入更多候选与冲突,并不必然降低失败率;A 错。放宽邻接约束、允许回溯都直接降低矛盾概率;B、C 对。去掉传播会破坏一致性,让输出几乎必然失败;D 错。
Q10. 下列哪项是 WFC 在游戏中的典型应用?
- A.实时计算物理碰撞
- B.自动生成地图关卡← 正确答案
- C.压缩角色贴图资源
- D.匹配玩家语音指令
答案:B。WFC 最经典的游戏应用就是把地形、房间、走廊当作瓦片,自动生成地图关卡。物理碰撞是引擎物理系统的工作,贴图压缩属于图像编码,语音指令匹配属于语音识别,都与 WFC 无直接关系。