# 用 AI 制作 2D 游戏地形：从贴图接缝到自然分布

规范网页：https://devtookit.com/article/8

作者：Revio

发布时间：2026-09-23T09:48:39Z

更新时间：2026-09-23T09:48:39Z

摘要：以 Godot 游戏 Survia 的雪地和冰湖为例，从 AI 材质的无缝拼接、大纹理采样，到 Dual Grid 地形边界与 Perlin 噪声分布，说明如何把生成贴图变成可用的 2D 游戏地形。

---

让 AI 画一张雪地或冰面，通常不难。把它铺满地图，问题才开始出现：同一块石头整齐地重复，冰面裂纹在格子边缘断开，雪岸的描边和实际轮廓错开。即使提示词写了“无缝贴图”，也需要把结果铺开检查。

本文以 Godot 游戏 Survia 的雪地和冰湖为例，介绍我们如何把 AI 生成的材质变成可用地形：先处理纹理的连续性，再计算不同地形的交界，最后用柏林噪声生成区域分布。做出的地面仍然使用 64 × 64 像素逻辑格，但纹理不必每格重复，边界也不必沿方格边缘转直角。

## 一、先分清：究竟是哪一种“拼不好”？

下面用项目里的同一张冰面材质做一个实验。三幅图都覆盖 1024 × 1024 像素，只改变图块的排列方式。

![同一冰面材质的三种排列：重复单块、随机打乱、按原位置铺设](/articles/survia-ai-2d-terrain/01-sampling-comparison.png)

*图 1：程序化对照实验，并非历史故障截图。A 的纹理以小格为周期重复；B 打破了重复顺序，却切断了裂纹；C 保留大纹理中各部分的位置关系。*

这三种铺法说明，**减少重复和消除接缝不是同一件事**。随机选图块能改变排列，但相邻边缘不匹配，接缝反而更多。

实际制作中，还要区分以下问题：

| 看到的现象 | 应检查什么 |
| --- | --- |
| 一到格子边缘，裂纹或明暗就断开 | 素材两侧是否连续、图块是否按正确位置排列 |
| 没有明显断缝，但像铺了一层壁纸 | 重复周期是否太短、是否有过于醒目的重复图案 |
| 雪与冰的直边正常，转角却缺块 | 邻接规则是否覆盖凸角、凹角和对角情况 |
| 边界线漂在材质外面，或整体偏半格 | 遮罩、描边、纹理是否使用同一套坐标 |
| 静止时正常，缩放后出现细线 | 图集采样是否读到了邻块或透明间隙 |

我们把地形拆成三部分处理：**材质回答“地面长什么样”，边界回答“这里到哪里为止”，分布回答“什么地方是什么地形”。** AI 主要提供材质和视觉参考；程序负责可重复的拼接规则。

## 二、同一种地面：用大纹理延长重复周期

### 先生成材质，不把场景画进贴图

生成雪地时，提示词可以约束为：

> 正交俯视的手绘雪地材质，冷蓝灰色，均匀柔和光照，细小积雪起伏；铺满画面，无透视、无地平线、无树木建筑、无边框；避免大块醒目标记，适合四向平铺。参考图用于统一笔触和颜色。

这能减少不合适的构图，但“四向平铺”仍是待验证的要求。树木、灌木、大石块最好单独生成和摆放：它们若被画进基础纹理，就会跟着纹理一起周期性出现。

Survia 的雪地、冰面等基础材质使用 **1024 × 1024 像素大纹理**，然后切成 16 × 16 个 64 像素图块。这里的 256 块，是一张图的不同位置，不是 256 张可以任意替换的变体。

地图坐标决定取哪一块：

```gdscript
# cell 是整张地图中的逻辑格坐标，不能用区块内部坐标代替。
var phase := Vector2i(posmod(cell.x, 16), posmod(cell.y, 16))
```

因此，地图上相邻两格也会取到大纹理中的相邻部分；走过 16 格后才重新开始一个周期。`posmod` 让负坐标也能得到 0～15 的有效索引。区块加载时继续使用全地图坐标，纹理便不会在区块边界重新起头。

它只是把重复周期从 64 像素拉长到 1024 像素，并没有消除重复。检查时应至少平铺 2 × 2 张大纹理，同时看近处接缝和远处图案规律。若仍能迅速认出同一条大裂纹，优先调整源图，或增加独立装饰，不要直接打乱这 256 个图块。

### 修复大纹理自身的接缝

大纹理内部连续，不代表左右、上下边缘也能接上。项目基础材质的处理方式是：保留中间区域，只在成对边缘附近做平滑混合。

以左右边缘为例，取距两侧相同距离的像素 `L`、`R`，求平均值 `M`：

```text
M  = (L + R) / 2
L' = lerp(M, L, w)
R' = lerp(M, R, w)
```

最外侧 `w = 0`，两边都变成 `M`；向内部移动，`w` 平滑增大到 1，逐渐保留原图。项目的 1024 像素材质使用 160 像素宽的处理带，权重为 `w = t²(3 − 2t)`，之后再处理上下边缘。这是本项目的参数，不是所有纹理的固定标准。

这种办法适合雪、沙、泥土等边缘允许柔化的材质。对横跨画面的粗裂纹或规则砖缝，它可能产生模糊或重影。此时可将图像循环平移半个宽高，把外侧接缝移到中心，再让图像编辑模型只修补中央接缝，保留外边缘，最后重新平铺检查。

本次检查中，现有雪地和冰面大纹理的左右、上下对应边缘像素差均为 0。这只能说明最外侧颜色对齐，不能证明边缘附近没有模糊，也不能证明大面积铺设不重复。

### 图集细线要在采样阶段处理

素材接好了，导入引擎后仍可能出现细线。线性过滤会读取采样位置周围的像素，如果图块紧挨着其他内容，边缘就可能混入错误颜色。

项目为每块 64 像素图块向外复制 2 像素边缘，即 **extrusion**。图块有效区域之间相隔 4 像素，图集外边距为 2 像素；有效内容仍然是 64 × 64，不能把扩出的边算进格子尺寸。Godot 的图集区域、边距和间隔应与打包参数一致，相关配置见 [TileSet 官方文档](https://docs.godotengine.org/en/stable/tutorials/2d/using_tilesets.html)。

边缘扩展解决的是过滤采样问题，修不了源图里的断裂纹。若使用 mipmap 或大幅缩小镜头，还要检查更低分辨率下是否串色；2 像素也不是对所有缩放比例都足够。

## 三、不同地形：由程序确定边界，再铺材质

雪地与冰湖相接时，我们没有让 AI 分别画直边、拐角，再猜它们能否拼起来。项目保留一张逻辑地图，记录每格是雪还是冰；显示层读取它，计算冰面应该覆盖的范围。

### 半格偏移，把格子中心变成边界采样点

方法叫 **Dual Grid（双网格）**。逻辑格照常保存地形，显示格相对它向右、向下偏移半格。这样，一个显示格的四个角，恰好落在四个相邻逻辑格的中心。

![逻辑地形格与半格偏移的显示网格，四个中心决定一块显示区域的边界](/articles/survia-ai-2d-terrain/02-dual-grid.png)

*图 2：蓝色表示冰，浅色表示雪。左右橙框是同一个显示格；它的四角读取四个逻辑格中心。右图由这些值计算轮廓，不再逐格绘制完整蓝色方块。*

对冰面来说，角落属于冰记作 1，否则记作 0。四个二值角共有 `2⁴ = 16` 种组合，也就是 **Corner16**。它可以表示直边、凸角、凹角，以及两种对角分布。半格偏移和四角编码的关系也可参照 [Tileset Roundup](https://www.boristhebrave.com/2013/07/14/tileset-roundup/)。

Survia 在着色器里对四个值做双线性插值，取 `0.5` 等值线作为边界。下面是基础公式，角顺序为左上、右上、右下、左下；省略了项目中的对角处理和边界扰动：

```glsl
float top = mix(corners.x, corners.y, uv.x);
float bottom = mix(corners.w, corners.z, uv.x);
float field = mix(top, bottom, uv.y);

float aa = max(fwidth(field) * 1.35, 0.006);
float coverage = smoothstep(0.5 - aa, 0.5 + aa, field);
```

`coverage` 控制冰面透明度，外侧露出下方雪地。相邻显示格共享两个角的值，所以沿共享边插值得到的数值一致，轮廓就能接上。这比要求 AI 在多张图片中精确画出相同边缘更容易验证。

对角排列还要单独约定：当左上、右下为冰时，中间是连接还是断开？项目选择连接目标地形，并只在格子内部增加小幅修正，修正量在四条边上归零，避免改变与邻格的接缝。

### 材质、遮罩和描边必须对齐

仅仅把显示格移动半格，会让纹理也跟着移动。项目因此同时修正材质采样：显示格里的纹理仍按原地图位置取样，需要跨过原图块边缘时，就取相邻图块对应的部分。**移动的是显示区域，地面纹理仍固定在地图坐标上。**

边界的手绘感来自两个补充：

- 用世界坐标噪声轻微扰动轮廓，让长边不那么机械。不能在每格都从 `(0, 0)` 重新算噪声，否则格子交界处又会断开。
- 在同一轮廓附近画暗线、内侧高光和接触阴影，让雪岸有厚度。

材质遮罩和描边共用 `terrain_boundary_field()`，避免各算一条边。若修改了轮廓噪声，却没同步修改描边，画面就会出现“冰面已经结束，边线还在外面”的错位。

![Godot 实际运行中的雪地、冰面与苔地交界，冰面裂纹和边界描边分别连续](/articles/survia-ai-2d-terrain/04-runtime-boundary.png)

*图 3：项目地形过渡阶段的运行截图。观察中央冰面的弯折边缘：内部裂纹来自连续材质，轮廓和深浅描边由显示层计算。截图保留了当时的角色与界面。*

这套方法仍有边界。Corner16 描述的是某种目标地形“有或没有”，多种材质同时接触时，还要确定覆盖顺序和允许的邻接关系。项目会在部分岩地与苔地之间插入碎石地作为过渡，避免为每种材质组合都制作一套规则。

视觉平滑也不等于碰撞变得平滑。普通地表的逻辑格仍负责通行、建造等规则；对于海岸这类玩家不能跨越的边缘，应从与视觉一致的边界生成阻挡，单独检查人物是否踩空或被透明区域挡住。

## 四、用柏林噪声决定地形分布

材质和边界解决以后，还剩一个问题：哪里放雪，哪里放岩石，哪里形成冰湖？

每格独立随机会得到满地碎斑。柏林噪声（Perlin Noise）的价值在于空间连续：邻近位置通常取得接近的数值，用阈值分类后容易形成区域。

它并非简单地插值随机灰度值。Perlin 在格点上生成梯度向量，将梯度与采样点的偏移向量做点积，再平滑插值。[Ken Perlin 的参考实现](https://mrl.cs.nyu.edu/~perlin/noise/)展示了这种计算方式。这里不需要把它理解成真实海拔模拟，把输出看作一张连续的“分布倾向图”即可。

![逐格随机、单层柏林噪声、多层fBm和坐标扭曲的同范围对比](/articles/survia-ai-2d-terrain/03-noise-fields.png)

*图 4：相同范围和色标下，独立随机形成碎点，Perlin 形成连续起伏；叠加多层后出现细节，扭曲坐标后区域形状进一步变化。B—D 调用项目生成器，尚未执行地形分类。*

### 从一层噪声到多尺度区域

只用一层噪声，形状容易过于平滑。项目叠加 5 层不同频率的噪声：每层频率乘 2，振幅乘 0.5，再按总振幅归一化。这种组合称为 **fBm**。低频决定大片区域，高频补充局部变化。

接着做 **Domain Warp（坐标扭曲）**：先用另外两张低频噪声算出横向、纵向偏移，再到偏移后的位置采样。

```text
warped_position = position + offset(position)
height = fbm(warped_position)
```

它改变的是采样位置，不是把地图图片事后拉伸。项目的基础海拔尺度为 58 格，扭曲尺度为 70 格，扭曲强度参数为 13 格。尺度越大，区域通常越宽；高频层越多，边缘细节越密。参数应以格子等明确单位记录，避免更换格子大小后地貌跟着变样。

如果直接在 Godot 内生成，`FastNoiseLite` 已提供 Perlin、fBm 和 Domain Warp。下面是基础 fBm 配置示例；它是便于读者起步的替代实现，不会逐点复现项目的 Python 生成器：

```gdscript
var noise := FastNoiseLite.new()
noise.seed = 732451
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.frequency = 1.0 / 58.0  # 以逻辑格坐标采样
noise.fractal_type = FastNoiseLite.FRACTAL_FBM
noise.fractal_octaves = 5
noise.fractal_lacunarity = 2.0
noise.fractal_gain = 0.5

var h := clampf(noise.get_noise_2d(cell.x, cell.y) * 0.5 + 0.5, 0.0, 1.0)
```

噪声类型、分形参数和坐标扭曲接口可查 [FastNoiseLite 官方文档](https://docs.godotengine.org/en/stable/classes/class_fastnoiselite.html)。

### 噪声提供候选分布，玩法约束决定最终地图

项目分别生成海拔、湿度、风力、森林适生度等场，再组合分类。例如，较高且风力适中的区域成为冻岩，较湿且避风的区域更适合苔地。分开这些场，能避免所有地形都只是同一张噪声的不同颜色条带。

但“低于阈值就是湖”仍可能产生许多小水坑。我们的冰湖先选择相互隔开的低洼湖心，再通过四邻接向外生长，优先扩展到分数较低的位置，并限制湖泊面积和间距。这样可以控制连通性，而不是期待某个随机种子刚好生成好看的湖。

噪声分类之后，还要清理过小区域、保护出生区和洞穴通路，再生成森林与资源。这里的区域清理发生在地图生成阶段；渲染阶段只显示最终地形，不应为了让轮廓更漂亮而偷偷修改地形归属。

![项目海岸版本的语义地图：大面积雪地、冰湖、岩地、苔地、海洋和地热区](/articles/survia-ai-2d-terrain/05-world-distribution.png)

*图 5：实际生成的地形分布预览。浅色为雪地，亮蓝为冰湖，绿色为苔地，深灰为冻岩，棕色为地热区，外围深蓝为海洋。图中的海岸和地热区还经过额外生成规则，并非单张 Perlin 噪声阈值化的结果。*

固定种子方便复现，但种子不是唯一条件。噪声算法、参数、规则顺序改变后，地图仍会改变，所以应同时保存生成器版本和配置。

## 五、想要不同效果，还可以怎么做？

以上方案适合大面积、连续的手绘地表，并不是所有 2D 游戏都需要自写边界着色器。

| 目标效果 | 可以采用的方案 | 需要付出的代价 |
| --- | --- | --- |
| 像素风草地、平台、墙体，边缘有明确人工造型 | 先规定拼接形状，再制作图块；用 Godot Terrain Peering Bits 自动选块 | 需要补齐邻接情况；引擎不会修复素材本身的接缝 |
| 凹凸边缘丰富的洞穴或实体区域 | Blob47：根据四边及有效对角邻居选块 | 通常需要 47 种非空邻接形态及额外空背景，制作量高于 Corner16 |
| 草逐渐过渡到泥、雪逐渐覆盖岩石 | 用连续权重混合多种材质，再加高度或细节遮罩 | 边缘更柔和，但容易混得发灰；碰撞与通行仍需单独定义 |
| 房间、道路、遗迹等有局部连接规则的结构 | 预制模块配合规则，或使用 WFC 约束相邻模块 | 必须先有合法模块；局部连接正确不代表全图可达，还可能出现求解冲突 |
| 小型固定关卡，希望完全控制构图 | 先定地图布局，再分区绘制，碰撞单独制作 | 构图自由，但扩图、动态开挖和地图复用更费工 |

Godot 的 Terrain 系统支持角、边以及角与边组合匹配；Blob 的有效对角规则可参照前面的 [图块方案说明](https://www.boristhebrave.com/2013/07/14/tileset-roundup/)。WFC 的用途和冲突情况见 [作者项目说明](https://github.com/mxgmn/WaveFunctionCollapse)。它们解决的问题不同，可以组合使用，例如噪声生成自然地貌，预制模块生成遗迹。

无论选择哪条路线，验收都应覆盖大面积重复、凸角与凹角、对角连接、负坐标、区块边界和镜头缩放。静态拼图检查素材；进入 Godot 后再检查采样、坐标和碰撞。

## 六、把这段要求复制给 AI

使用前补充参考图和项目路径。下面以雪地与冰面两种地形为最小目标，先验证拼接，再扩展其他材质：

```text
请在我的 Godot 4 项目中制作一套正交俯视 2D 雪地与冰面地形。
先读取现有项目和参考图，复用已有资源，不覆盖已确认素材。

1. AI 负责生成风格一致的地面材质，树木、石块和建筑独立制作。
   材质不包含透视、边框、场景物体或醒目的重复标记。
2. 逻辑格使用 64 px。基础材质制作成 1024 px 周期纹理，修复
   左右和上下接缝，再切成 16×16 块。按全地图格坐标正模取样，
   不随机打乱这些图块；提供至少 2×2 周期的平铺预览。
3. 图集每块扩边 2 px，有效区仍为 64 px；正确配置边距、间隔、
   过滤。检查镜头缩放后的细线，不用模糊整张图掩盖问题。
4. 地形 ID 与显示分开。用半格偏移 Dual Grid 读取四个逻辑格
   中心，构造 Corner16 边界；固定对角连接规则。遮罩与描边
   共用边界函数和世界坐标噪声，纹理保持地图坐标对齐。
5. 固定种子，用 Perlin + fBm 生成连续区域，可加低频坐标扭曲。
   湖泊另外约束连通性、面积和间距，保护出生点和必要通路。
   地图生成后再放置装饰，不靠装饰遮住错误接缝。
6. 分步交付：材质平铺对照、16 种边界示意、地形分布预览、
   Godot 运行画面。测试共享边、负坐标、跨区块和视觉/碰撞关系。
   说明参数、实现限制和未执行检查，附可重复运行的生成脚本。
```

这段要求把 AI 的工作拆成了材质、边界和地图三个可检查的结果。出现问题时，就能针对某一层修改：裂纹断开查材质取样，岸线错位查边界坐标，满地碎斑查分布规则。
