波函数坍缩(Wave Function Collapse)

Rust + WebAssembly 按相邻规则逐格「坍缩」,从几条规则或一张小样例图生成无限变化的地图

模糊的格子还有多种可能,清晰的格子已经确定  |  右边是规则的来源:平铺模型的全部图块,或样例模型学习的小图

📖 原理说明

波函数坍缩(Wave Function Collapse,Maxim Gumin 2016)是一种程序化生成算法,名字借自量子力学:一开始每个格子都「同时是所有可能的图块」,每一步挑一个格子让它「坍缩」成确定的图块,再把这个决定带来的限制传播给周围的格子,直到整张地图都确定下来。规则只有一条:任意两个相邻的格子必须是允许相邻的组合。页面上模糊的颜色就是某个格子剩下的所有可能的平均色。

🧮算法原理

H = log Σw − (Σ w log w) / Σw香农熵:可能性越少、越集中的格子熵越小,最先坍缩
可能(邻居) ← 可能(邻居) ∩ ⋃p ∈ 可能(本格) 允许(p, 方向)约束传播:邻居只能保留与本格某个剩余可能兼容的图块,有变化就继续向外传
熵最小优先
每次挑选剩余可能最少的格子来决定,并按图块权重随机选一个。先处理最受约束的地方,就像解数独时先填只剩一个候选的格子,能大大减少走进死胡同的机会,也让地图像晶体一样从已确定的区域向外生长。
平铺模型
直接给出图块和「谁能挨着谁」。管道图块看每条边上有没有管道;地形用的是角点图块:每块的四个角各有一个高度,相邻图块共享的两个角必须相同,同一块的四个角最多差一级,渲染时对四个角插值,所以海岸线是连续的。如果改成「每格一个高度、相邻最多差一级」,规则太弱,生成出来只是一片噪点。
重叠模型(从样例学习)
不手写规则,而是给一张小样例图:把其中所有 3×3 的小块连同它们的 4 种旋转和镜像都收集起来当作「图块」,出现得越多权重越大;两个小块错开一格后重叠部分完全相同,才允许相邻。输出的每个像素取所在小块的左上角颜色,于是新图的任何局部都像样例里的某一处,整体却从未出现过。
矛盾与回溯
传播可能把某个格子的可能性删光,这就是矛盾。求解器在每次坍缩前保存一份快照(最多 48 份),遇到矛盾就退回上一步,把刚才的选择排除掉再试;退无可退就换一个随机种子从头开始,重来 20 次仍失败才宣告无解。示例图块集设计得比较宽松,通常一次就成功。
无缝平铺
勾选后,左右边缘和上下边缘也被当作相邻来检查,生成的地图可以像壁纸一样无缝拼接。

🔄Rust 与 JavaScript 的分工

  1. JS选择图块集和大小后调用 wfc_new(图块集, 宽, 高, 种子, 是否平铺);右侧的图例来自 wfc_preview()。
  2. Rust每一帧 JS 调用 wfc_step(n):Rust 坍缩 n 个格子,每次都做完整的约束传播、必要时回溯或重来,返回状态、已确定的格子数和回溯次数。每个格子的可能集合用位集存储,传播时按 64 位一组做与、或运算。
  3. JSwfc_image() 返回整张图:已确定的格子画图块,未确定的格子画剩余可能的加权平均色,JS 放大显示,保持像素锐利。

⚡性能要点

  • 实测(桌面 Chromium,40×40 格):管道 16 种图块约 13ms 生成完,地形 76 种约 40ms,迷宫 120 种约 90ms,湖泊 265 种约 350ms。图案越多,每个格子的位集越长,传播时要合并的集合也越多。页面默认每帧只确定 8 格,是为了能看清生长过程;「直接完成」一次跑到底。
  • 传播是这个算法的主要开销:一次坍缩可能引发连锁反应,一直波及很远的格子。更快的实现会为每个图案维护「还剩几个兼容邻居」的计数,这里为了代码清晰,直接对集合求并。

源码crates/graphics/wfc/src/solver.rscrates/graphics/wfc/src/model.rscrates/graphics/wfc/src/tilesets.rswww/wfc/index.js