如果你玩过《我的世界》或任何一款沙盒游戏,一定感叹过:这么辽阔的地图,难道是一块砖一块砖手摆出来的吗?当然不是。游戏开发者早就在用“程序化生成”来凭空创造世界——但早期手法更像“撒一把沙子让它自然堆积”,能生成很多,却很难生成“有风格、有组织”的东西。 现在有一台神奇的“智能织布机”,叫作 Wave Function Collapse(波函数坍缩,简称 WFC)。它和我们想象中的 AI 不太一样:你不需要给它上百万张图片做训练,只需要递进去一小块“碎花布”——比如一张 8×8 的手绘小地图——它就能自己总结出花纹规律,然后哗啦哗啦织出一整匹风格统一的大地图。 这台织布机最迷人的地方,是它从量子力学里偷师:在确定之前,每一块地皮都像薛定谔的猫,“既是草地又是森林又是水面”;每确定一块,旁边的地皮就规规矩矩收敛一分。今天这篇文章,我们就来拆开这台织布机,看看它是怎么从一小块碎布,织出一整个世界的。
学会 WFC,你就掌握了一种“用局部规则生成全局结构”的工程武器:它可以帮你批量产出风格统一的游戏关卡、3D 场景和纹理变体,是小团队做内容工业化的重要利器。
目录
可视化演示
交互展示WFC算法从超position初始化到约束传播逐格坍缩的完整流程及瓦片邻接规则定义。
本节配有交互动效(进入视口后自动加载,约 3KB)。
超position:所有格多重状态叠加 6×6网格每格包含5种可能瓦片状态 熵最大时每格等概率处于任意状态 H(cell) = -Σ P(tile) · log P(tile) 初始状态所有格子处于最大熵叠加态等待观测坍缩
算法起源:量子灵感的种子
在织布机发明之前,织布全靠工匠一梭一梭地排线。手里明明有一块漂亮的碎花布,想复制同样的花纹,却只能埋头苦干几天几夜。Wave Function Collapse 就是一台“会看样学样”的织机:你递进去一小块碎布,它自己提取花纹规律,转眼织出一整匹风格统一的布。
2016 年,Maxim Gumin 发布了这个开源项目,名字直接取自量子力学。在量子世界里,一个粒子在被观测前可以同时处于多个位置——这叫“叠加态”;观测的瞬间,它才“坍缩”为一个确定状态。WFC 把这套语言借了过来:每个格子一开始“既是草地又是森林又是水”,生成的过程就是逐个观测它、让它坍缩,直到整张地图尘埃落定。
程序化内容生成(PCG)在游戏里并不新鲜,Perlin 噪声、随机砖块都能生成地图。但传统方法很难复刻美术风格:你给它看一张手绘村庄,它学不会村庄的模样。WFC 的核心价值正在于“样子驱动生成”——只要有一张手工小样,就有机会用同样的风格变出无穷地图。独立游戏《Townscaper》就是这套思路的代表作。
碎花布再美,也要有人读懂花纹才能复制。WFC 学会了量子力学的语言,它就把“读懂”变成了可计算的流程——这正是我们从下一节开始逐层拆解的零件。
- 波函数符号
|\psi\rangle读作“psi“,代表格子当前那团”尚未决定的可能状态”,好比织布机上一根悬在半空、颜色未定的线。 - 求和符号
\sum_{k=1}^{K}表示把这 K 个候选状态全部装进“可能集合”里,一个都不丢。 - 权重系数
w_k是第 k 个候选状态的分量,权重越大,它越容易被织布匠选中。 - 具体状态
|k\rangle代表第 k 种具体状态,比如“这块地是草地”或“这格是水面”。
核心概念:一根线的叠加态
织一匹布时,每一根纬线在穿过经线之前,都可以是任意颜色——它“叠加”了所有颜色在身。织布匠的手一落,它才“坍缩”成一种颜色。更重要的是,这根线的颜色一旦定下来,旁边的线就不能随便选了,必须符合花纹规律,否则布面就会乱成一团。
WFC 里对应的四个核心词可以这样记:格子就是布面上的一个位置;格子里的“可能性列表”就是叠加态;从列表里选定一个结果,就是坍缩;确定后清理邻居的不可能选项,就是约束传播。还有一个度量叫“熵”:可能列表越短,信息熵越低,代表这块地方越接近“板上钉钉”。
为什么熵很重要?想象你面前有两个格子:一个还剩 10 种可能,一个只剩 2 种。聪明的织布匠一定会先处理只剩 2 种的格子——它几乎已经明确了,先定下来几乎不会错。WFC 每一轮都优先挑选熵最低的格子,这正是算法稳定不崩的秘密之一。
一根线悬在半空时,谁也不知道它是什么颜色;织布匠落手之后,它才从“无数可能”变成“一个事实”。WFC 的每一步,都是在重复这个朴素动作。
- 熵符号
S_i表示第 i 个格子的不确定性,值越大越混乱。 - 负号与求和
-\sum_{c \in C_i}对当前候选集合里的每个候选做累加,负号保证熵是正数。 - 候选概率
p_{i,c}是格子 i 选择候选 c 的概率,由权重归一化得到。 - 对数项
\log_2 p_{i,c}用二进制对数把概率变成信息量,可能性越少,信息量越小。
核心流程:织布的四个动作
有经验的织布匠不会从左到右硬织,而是先找花纹最清晰、最没悬念的区域落手,再像水面涟漪一样向外扩散。先定“铁板钉钉”的,再顺藤摸瓜——WFC 的主循环正是这么设计的。
整个流程可以拆成四步。第一步是初始化:每个格子都持有全部候选模式,也就是所有格子都处于叠加态。第二步是选择:找出熵最低、最接近确定的那个格子。第三步是坍缩:按权重随机给它一个确定答案。第四步是传播:把新答案带来的约束传递给上下左右邻居,把邻居中不可能出现的模式清扫出去。如果扫到某个格子的候选列表变成空的,说明出现了矛盾,算法就要回退或换随机种子重来。
这个“选择—坍缩—传播”的循环会一直进行,直到所有格子都有了明确答案。优先选择熵最低的格子,就像先拼拼图里图案最明显的那几块——确定性强,出错概率小,后面的约束也会更顺畅。
织布匠落手快,但绝不乱落:越是常见的花纹越容易织出来。这一份“按经验出牌”的随机,就是 WFC 风格统一的底气。
- 选择概率
P(\text{选择 } c)表示候选模式 c 被挑中的概率。 - 当前权重
w_c是模式 c 在样例里出现的次数,出现越多越常见。 - 归一化分母
\sum_{j \in C} w_j把当前格子所有候选模式的权重加起来,保证概率总和为 1。
邻接规则:花纹的语法
碎布上的“红花旁边一定配绿叶”,就是花纹的语法。如果你记住了这一句,就能推理出一块更大的花布大致应该长什么样。WFC 的邻接规则,就是给机器学语法用的。
具体做法用一个词概括:滑窗提取。拿一个 2×2 的小窗口,从样例图的左上角滑到右下角,每滑到一处就记下一个“模式块”,同时统计它出现的次数。接着检查每两个模式块能否左右相邻或上下相邻:如果它们的接缝像素完全吻合,就在邻接表里记一笔“合法”。
有了这张邻接表,生成阶段就简单了:每次某个格子确定了一种模式,就去查表,把邻居中所有不合法的模式划掉。这张表是单向的还不够,实战中往往还要额外建立反向邻接表,让约束既能从“我”流向“右边”,也能从“右边”流回“我”,四方向传播才跑得起来。
邻接表就是织布匠脑子里那本“花纹词典”。每落一梭,他都要悄悄翻一页词典;每翻一页词典,一整片布面的命运就被悄悄改写了。
- 兼容性函数
\text{Compatible}_{\rightarrow}(A, B)回答一个问题:“A 的右边能接 B 吗?” - 乘积符号
\prod_{r=0}^{S-1}表示逐行检查,只要有一行不等,乘积就归零,立刻判定不合法。 - 边界相等条件
[A[r][S-1] = B[r][0]]是一个逻辑判断:A 的最后一列和 B 的第一列必须一模一样。
参数调优:织布匠的旋钮
一台好的织布机必须有几个顺手的旋钮:图案单元多大、要不要对称变换、布边要不要无缝衔接、换一个种子是不是又能织出新花样。WFC 的参数调优,本质上就是调这些旋钮。
最基础的是输出尺寸,也就是你想织多大一块布;其次是模式尺寸 S×S——S 越小,生成越自由但花纹容易碎;S 越大,纹样越保真但几乎是在“复读”样例。许多成熟方案会给模式增加旋转和镜像变体,让一个“红花”变成四个朝向,多样性立刻翻四倍。
周期模式是另一个常用旋钮:打开后,布匹的左边和右边缝合,上边和下边缝合,生成结果是能无限平铺的无缝贴图。种子则控制整台机器的随机序列:同一套参数、同一个种子,可以复现同一块布;换颗种子,又得到一块风格相同但纹路各异的料子。
旋钮调得好,织出来是桌布窗帘;调得不好,就是一团乱线头。WFC 的工程魅力,就在于同一个核心算法配不同参数,能适配截然不同的美术风格。
- 坐标函数
T(x, y)表示坐标 (x, y) 处的花纹类型。 - 平移偏移
\Delta x,\ \Delta y是平移量,比如向右平移 16 格。 - 取模运算
\bmod W,\ \bmod H把坐标绕回地图宽度和高度之内,让边缘首尾相连。
应用实践:从碎布到大千世界
当你终于能织出平整又有风格的一匹布,下一步自然是织更大的东西:2D 像素地图是“平面织法”,3D 场景就是“立体编织”。WFC 的实际应用,正是沿着这个维度一路外推的。
在 2D 像素时代,最常见的是平台跳跃关卡:拿一小块手工绘制的关卡切片当样例,让 WFC 生成一整张岩石、砖块、陷阱和金币分布合理的关卡。在 3D 场景里,把 2×2 模式升级为 2×2×2 体素模式,邻接表从四方向扩展到六方向,就能生成建筑群落、洞穴和室内家具布局。
但游戏关卡不是“好看”就够,还要能玩:必须有通路、有敌人、有补给,难度曲线要合理。每一个设计规则都是一个约束集合,真正可玩的地图,必须落在所有约束集合的交集里。这也是 WFC 与纯美术生成工具最大的不同——它输出的每一个方案,都要被你叠加的规则反复检验。
从一块碎花布到一整座城堡,WFC 用一条朴素的道理贯穿始终:先有规则,再有世界。你递给它的每一块碎布,都在替你说出——这个世界应该长成什么样。
- 规则集合
C_k是第 k 条设计规则约束下的合法地图集合,比如“所有金币必须落在可跳跃的台子上”。 - 交集符号
\bigcap_{k=1}^{M}表示把所有规则集合重叠起来,只有全部规则都满足的地图才算数。 - 可玩集合
C_{\text{playable}}是最终可玩地图的集合,WFC 的搜索其实就在这个交集中进行。
AI 应用场景
批量生成平台跳跃关卡
阶段:游戏内容生产(程序化内容生成)
解决的问题:手工搭建平台跳跃关卡需要逐帧摆砖块、敌人和奖励,耗时巨大;传统随机生成法又容易产生无法通关的死路。
具体案例:某独立游戏团队用 8×8 手绘样板生成 200 个关卡片段,每个片段由 WFC 计算砖块、尖刺和金币的邻接规则,再拼装成完整关卡。玩家跑起来风格统一,但每条路线都有新变化。
开放世界地表与植被覆盖
阶段:关卡设计(空间布局)
解决的问题:开放世界的地表要呈现“草地→森林→水泊”的自然过渡,手工刷笔刷费时费力;纯随机撒点又会让过渡生硬突兀。
具体案例:手工绘制一张小样图,定义草地、树木、岩石、水的相邻关系,再用 WFC 生成 512×512 的地表类型图,叠加噪声微调后,5 分钟就能得到一版生态合理、风格统一的世界地表。
3D 室内场景摆件生成
阶段:3D 场景生成(体素填充)
解决的问题:在大型室内空间手工摆放大量家具——桌椅、柜台、箱子——还要保证不堵门、不穿墙,工作量巨大。
具体案例:在体素网格上定义“椅子必须靠墙”“桌子不能堵门”“箱子不挡路”三类邻接约束,WFC 在室内自动生成家具布局。策划只需检查结果并微调,不需要再逐个摆件。
Python 演示
import random T = [[0, 0, 0, 1], [0, 0, 1, 1], [0, 0, 1, 2], [0, 0, 1, 2]]S, W = 2, 6 def extract(): pats, cnt = [], {} for i in range(3): for j in range(3): p = tuple(T[i+x][j+y] for x in range(S) for y in range(S)) if p not in cnt: pats.append(p) cnt[p] = cnt.get(p, 0) + 1 return pats, cnt pats, cnt = extract()weights = [cnt[p] for p in pats] def build_adj(pats): P = len(pats) right = [set() for _ in range(P)] down = [set() for _ in range(P)] for a in range(P): for b in range(P): if pats[a][1] == pats[b][0] and pats[a][3] == pats[b][2]: right[a].add(b) if pats[a][2] == pats[b][0] and pats[a][3] == pats[b][1]: down[a].add(b) left = [set() for _ in range(P)] up = [set() for _ in range(P)] for a in range(P): for b in right[a]: left[b].add(a) for b in down[a]: up[b].add(a) return right, down, left, up right, down, left, up = build_adj(pats)DIRS = [(1, 0, right), (-1, 0, left), (0, 1, down), (0, -1, up)] def run(seed): random.seed(seed) wave = [[list(range(len(right))) for _ in range(W)] for _ in range(W)] stack = [] def prop(): while stack: r, c = stack.pop() prev = wave[r][c] for dr, dc, adj in DIRS: nr, nc = r + dr, c + dc if 0 <= nr < W and 0 <= nc < W: ok = set() for p in prev: ok |= adj[p] nxt = [p for p in wave[nr][nc] if p in ok] if nxt != wave[nr][nc]: wave[nr][nc] = nxt stack.append((nr, nc)) while True: cells = [(r, c, len(wave[r][c])) for r in range(W) for c in range(W) if len(wave[r][c]) > 1] if not cells: break r, c, _ = min(cells, key=lambda t: t[2]) chosen = random.choices( wave[r][c], weights=[weights[p] for p in wave[r][c]])[0] wave[r][c] = [chosen] stack.append((r, c)) prop() if any(not x for row in wave for x in row): return None return wave wave = Nonefor s in range(30): wave = run(s) if wave is not None: break if wave is None: print("生成失败:请调整样例图或模式尺寸")else: for r in range(W): print(' '.join(str(pats[wave[r][c][0]][0]) for c in range(W)))
p = tuple(T[i+x][j+y] for x in range(S) for y in range(S))用 2×2 滑窗在样例图上提取一个模式块。cnt[p] = cnt.get(p, 0) + 1统计每个模式块出现的次数,作为后续抽签的权重。weights = [cnt[p] for p in pats]把模式列表映射成权重列表,出现越多越容易被选中。if pats[a][1] == pats[b][0] and pats[a][3] == pats[b][2]:判断模式 B 的左边一列能否接在 A 的右边一列上。for b in right[a]: left[b].add(a)由“右邻”表反推出“左邻”表,让约束能向四个方向传播。cells = [(r, c, len(wave[r][c])) ... if len(wave[r][c]) > 1]找出所有处于叠加态、还没确定的格子。r, c, _ = min(cells, key=lambda t: t[2])选择熵最低(候选模式最少)的格子优先处理。chosen = random.choices(wave[r][c], weights=[weights[p] for p in wave[r][c]])[0]按权重随机坍缩,保留样例的风格倾向。nxt = [p for p in wave[nr][nc] if p in ok]用邻接表修剪邻居的候选模式,这就是约束传播。if any(not x for row in wave for x in row): return None一旦有格子的候选清空,说明矛盾,立即返回失败让上层换种子重试。
常见误区
误区听到“AI 生成地图”,就以为它像 ChatGPT 一样需要先收集大量数据、训练一个神经网络模型,才能开始生成。
正解WFC 完全不需要训练,也不需要反向传播。它只做两件事:从样例里“提取规则”,再在规则上做“约束推理”。这更像是给织布机安装一张提花卡,机制完全透明、可解释。
为何常见:近几年 AI 话题几乎都被“训练”占领,看到“生成”两个字就自动联想到深度学习模型,反而忽略了程序化生成这条经典路线。
误区反正已经选出了格子的确定答案,邻居约束晚点一起处理不也一样吗?是不是可以省掉传播这步?
正解传播是 WFC 正确性的核心,不是加速技巧。每坍缩一个格子,必须立刻把邻居候选修剪干净,否则后续选择会建立在过期信息上,整张地图很快矛盾崩溃。
为何常见:传播看起来很像是“剪枝”这类优化手段,初学者容易把它归类为锦上添花,而不是算法的骨架之一。
误区生成的图案千篇一律,会不会是因为规则太多?把邻接关系放松一点,结果是不是更随机、更丰富?
正解完全没有约束时,WFC 退化成纯随机填充,谈不上风格;约束过强则频繁矛盾重试。好的结果往往来自“中等强度的规则 + 有倾向的权重”:规则定义风格,权重控制偏向。
为何常见:邻接表初看是一堆条条框框,让人误以为它限制了多样性,却忘了风格恰恰是由约束定义出来的。
误区有了 WFC,策划手工设计关卡的时代就结束了,所有玩法结构都能自动生成。
正解WFC 擅长“局部相似、整体扩展”,但它不关心可玩性——路径是否通畅、难度曲线是否合理、叙事节奏是否连贯,这些都不在邻接规则里。成熟的做法是策划搭好关卡骨架,WFC 负责填充风格统一的血肉。
为何常见:看到 WFC 能生成漂亮地图,容易把“生成风景”和“设计关卡”混为一谈,忽略了可玩性需要大量显式规则约束。
误区WFC 的演示全是像素格子,是不是只能做平面小地图,做不了 3D 大场景?
正解把模式从 S×S 扩展到 S×S×S,邻接从四个方向扩展到六个方向,WFC 就能生成体素建筑、洞穴和室内布局。社区里已有大量 3D 生成案例,原理和 2D 完全同构。
为何常见:2D 像素样例最直观、最容易画,大家看到的 demo 因此集中在 2D,但只要理解了模式与邻接的抽象,维度的扩展是自然而然的事。
面试题
Q1. WFC 的核心输入是什么?
- A.一张完整的生成目标地图
- B.一小块包含局部规则的样例图← 正确答案
- C.一组训练好的神经网络权重
- D.一串预先排好的随机数种子
答案:B。WFC 从样例图中提取模式与邻接规则,再按规则生成更大的地图,因此核心输入是一块“碎花布”样例。它不需要完整目标图,不需要神经网络,也不需要预设随机串。
Q2. “坍缩”在 WFC 中指的是什么?
- A.格子从多种可能变为确定一种← 正确答案
- B.把所有已确定的格子全部删除
- C.将样例图缩放到更大的尺寸
- D.压缩地图文件的存储体积
答案:A。坍缩就是织布匠落手的一刻:格子的候选列表从多个变成一个确定模式。删除格子、缩放图片或压缩文件都与 WFC 的坍缩无关。
Q3. WFC 为什么优先选择“熵最低”的格子处理?
- A.熵最低的格子颜色最鲜艳
- B.熵最低的格子计算速度最快
- C.熵最低的格子错误代价最小← 正确答案
- D.熵最低的格子权重数值最大
答案:C。熵表示不确定性。熵最低的格子只剩少数几种可能,随机选错的风险最小,先把它定下来能让后续约束传播更稳定,整体失败率更低。
Q4. WFC 的邻接规则通常从哪里获取?
- A.用户手工输入每个格子的邻接表
- B.从样例图滑窗提取模式自动统计← 正确答案
- C.从随机数生成器自动推断规律
- D.由训练好的神经网络反向传播得到
答案:B。标准 WFC 用滑窗扫描样例图,提取模式块并统计共现关系,自动生成邻接表。手工逐格输入太慢,随机推断不靠谱,神经网络也不是 WFC 的机制。
Q5. WFC 生成过程中出现矛盾时,常见处理策略是什么?
- A.跳过矛盾的格子继续向后生成
- B.回退或更换随机种子重头再来← 正确答案
- C.把矛盾的格子直接涂成黑色
- D.增大输出地图的网格尺寸
答案:B。矛盾说明当前约束无法同时满足,继续硬填只会越陷越深。标准做法是放弃本轮,换种子或回滚重新生成;增大尺寸无法解决规则本身的冲突。
Q6. 模式权重在 WFC 生成中起什么作用?
- A.控制生成时的选择概率,影响风格← 正确答案
- B.决定格子渲染颜色的明暗亮度
- C.加速邻接表构建的遍历速度
- D.保证生成地图一定四通八达
答案:A。权重来自模式在样例中出现的频率,频率越高越容易被选中,从而让生成结果继承样例的整体风格。它既不参与渲染,也不保证连通性。
Q7. 以下哪些做法能提高 WFC 生成结果的多样性?
- A.对模式做旋转和翻转变体← 正确答案
- B.提供多张不同风格的样例图← 正确答案
- C.把邻接规则写得尽可能严格
- D.更换随机种子重新生成← 正确答案
答案:ABD。旋转镜像能扩大模式库,多样例让风格混合,换种子会得到不同随机序列。而把邻接规则写得太严格会让候选空间变小,反而更容易矛盾或死板。
Q8. 关于 WFC 与手工关卡设计的关系,下列说法正确的是?
- A.WFC 适合批量生成风格统一的原型← 正确答案
- B.手工设计更便于把控叙事与节奏← 正确答案
- C.WFC 可以完全替代关卡策划创意
- D.手工布局加 WFC 填充是常见组合← 正确答案
答案:ABD。WFC 擅长局部规则驱动的批量填充,手工设计擅长全局叙事与体验把控,两者是互补关系。策划搭骨架、WFC 填血肉的组合在工业界很常见,完全替代的说法不成立。
Q9. WFC 的约束传播阶段主要在做什么?
- A.把样例图广播给所有等待生成的格子
- B.用邻居的确定结果修剪不可能的模式← 正确答案
- C.把模式权重累加到下一轮随机选择里
- D.将已生成格子移动到画布正中央
答案:B。当某格子坍缩后,传播阶段会检查它的上下左右邻居,将不满足邻接表的候选模式从邻居列表中移除。这一过程可能连锁反应,所以用队列逐个处理。
Q10. 3D WFC 相比 2D 的主要挑战有哪些?
- A.邻接方向从四个扩展到六个← 正确答案
- B.模式提取维度从二维升到三维← 正确答案
- C.输出网格尺寸完全不受控制
- D.候选状态增加导致性能压力变大← 正确答案
答案:ABD。3D 场景不仅多出上下两个邻接方向,模式块和候选状态数量也会显著膨胀,推理与传播的开销随之增大。输出网格尺寸依然由参数控制,并非失控。