用 AI 制作 2D 游戏地形:从贴图接缝到自然分布
Revio ·
以 Godot 游戏 Survia 的雪地和冰湖为例,从 AI 材质的无缝拼接、大纹理采样,到 Dual Grid 地形边界与 Perlin 噪声分布,说明如何把生成贴图变成可用的 2D 游戏地形。
让 AI 画一张雪地或冰面,通常不难。把它铺满地图,问题才开始出现:同一块石头整齐地重复,冰面裂纹在格子边缘断开,雪岸的描边和实际轮廓错开。即使提示词写了“无缝贴图”,也需要把结果铺开检查。
本文以 Godot 游戏 Survia 的雪地和冰湖为例,介绍我们如何把 AI 生成的材质变成可用地形:先处理纹理的连续性,再计算不同地形的交界,最后用柏林噪声生成区域分布。做出的地面仍然使用 64 × 64 像素逻辑格,但纹理不必每格重复,边界也不必沿方格边缘转直角。
一、先分清:究竟是哪一种“拼不好”?
下面用项目里的同一张冰面材质做一个实验。三幅图都覆盖 1024 × 1024 像素,只改变图块的排列方式。

图 1:程序化对照实验,并非历史故障截图。A 的纹理以小格为周期重复;B 打破了重复顺序,却切断了裂纹;C 保留大纹理中各部分的位置关系。
这三种铺法说明,减少重复和消除接缝不是同一件事。随机选图块能改变排列,但相邻边缘不匹配,接缝反而更多。
实际制作中,还要区分以下问题:
| 看到的现象 | 应检查什么 |
|---|---|
| 一到格子边缘,裂纹或明暗就断开 | 素材两侧是否连续、图块是否按正确位置排列 |
| 没有明显断缝,但像铺了一层壁纸 | 重复周期是否太短、是否有过于醒目的重复图案 |
| 雪与冰的直边正常,转角却缺块 | 邻接规则是否覆盖凸角、凹角和对角情况 |
| 边界线漂在材质外面,或整体偏半格 | 遮罩、描边、纹理是否使用同一套坐标 |
| 静止时正常,缩放后出现细线 | 图集采样是否读到了邻块或透明间隙 |
我们把地形拆成三部分处理:材质回答“地面长什么样”,边界回答“这里到哪里为止”,分布回答“什么地方是什么地形”。 AI 主要提供材质和视觉参考;程序负责可重复的拼接规则。
二、同一种地面:用大纹理延长重复周期
先生成材质,不把场景画进贴图
生成雪地时,提示词可以约束为:
正交俯视的手绘雪地材质,冷蓝灰色,均匀柔和光照,细小积雪起伏;铺满画面,无透视、无地平线、无树木建筑、无边框;避免大块醒目标记,适合四向平铺。参考图用于统一笔触和颜色。
这能减少不合适的构图,但“四向平铺”仍是待验证的要求。树木、灌木、大石块最好单独生成和摆放:它们若被画进基础纹理,就会跟着纹理一起周期性出现。
Survia 的雪地、冰面等基础材质使用 1024 × 1024 像素大纹理,然后切成 16 × 16 个 64 像素图块。这里的 256 块,是一张图的不同位置,不是 256 张可以任意替换的变体。
地图坐标决定取哪一块:
# cell 是整张地图中的逻辑格坐标,不能用区块内部坐标代替。
var phase := Vector2i(posmod(cell.x, 16), posmod(cell.y, 16))
因此,地图上相邻两格也会取到大纹理中的相邻部分;走过 16 格后才重新开始一个周期。posmod 让负坐标也能得到 0~15 的有效索引。区块加载时继续使用全地图坐标,纹理便不会在区块边界重新起头。
它只是把重复周期从 64 像素拉长到 1024 像素,并没有消除重复。检查时应至少平铺 2 × 2 张大纹理,同时看近处接缝和远处图案规律。若仍能迅速认出同一条大裂纹,优先调整源图,或增加独立装饰,不要直接打乱这 256 个图块。
修复大纹理自身的接缝
大纹理内部连续,不代表左右、上下边缘也能接上。项目基础材质的处理方式是:保留中间区域,只在成对边缘附近做平滑混合。
以左右边缘为例,取距两侧相同距离的像素 L、R,求平均值 M:
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 官方文档。
边缘扩展解决的是过滤采样问题,修不了源图里的断裂纹。若使用 mipmap 或大幅缩小镜头,还要检查更低分辨率下是否串色;2 像素也不是对所有缩放比例都足够。
三、不同地形:由程序确定边界,再铺材质
雪地与冰湖相接时,我们没有让 AI 分别画直边、拐角,再猜它们能否拼起来。项目保留一张逻辑地图,记录每格是雪还是冰;显示层读取它,计算冰面应该覆盖的范围。
半格偏移,把格子中心变成边界采样点
方法叫 Dual Grid(双网格)。逻辑格照常保存地形,显示格相对它向右、向下偏移半格。这样,一个显示格的四个角,恰好落在四个相邻逻辑格的中心。

图 2:蓝色表示冰,浅色表示雪。左右橙框是同一个显示格;它的四角读取四个逻辑格中心。右图由这些值计算轮廓,不再逐格绘制完整蓝色方块。
对冰面来说,角落属于冰记作 1,否则记作 0。四个二值角共有 2⁴ = 16 种组合,也就是 Corner16。它可以表示直边、凸角、凹角,以及两种对角分布。半格偏移和四角编码的关系也可参照 Tileset Roundup。
Survia 在着色器里对四个值做双线性插值,取 0.5 等值线作为边界。下面是基础公式,角顺序为左上、右上、右下、左下;省略了项目中的对角处理和边界扰动:
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(),避免各算一条边。若修改了轮廓噪声,却没同步修改描边,画面就会出现“冰面已经结束,边线还在外面”的错位。

图 3:项目地形过渡阶段的运行截图。观察中央冰面的弯折边缘:内部裂纹来自连续材质,轮廓和深浅描边由显示层计算。截图保留了当时的角色与界面。
这套方法仍有边界。Corner16 描述的是某种目标地形“有或没有”,多种材质同时接触时,还要确定覆盖顺序和允许的邻接关系。项目会在部分岩地与苔地之间插入碎石地作为过渡,避免为每种材质组合都制作一套规则。
视觉平滑也不等于碰撞变得平滑。普通地表的逻辑格仍负责通行、建造等规则;对于海岸这类玩家不能跨越的边缘,应从与视觉一致的边界生成阻挡,单独检查人物是否踩空或被透明区域挡住。
四、用柏林噪声决定地形分布
材质和边界解决以后,还剩一个问题:哪里放雪,哪里放岩石,哪里形成冰湖?
每格独立随机会得到满地碎斑。柏林噪声(Perlin Noise)的价值在于空间连续:邻近位置通常取得接近的数值,用阈值分类后容易形成区域。
它并非简单地插值随机灰度值。Perlin 在格点上生成梯度向量,将梯度与采样点的偏移向量做点积,再平滑插值。Ken Perlin 的参考实现展示了这种计算方式。这里不需要把它理解成真实海拔模拟,把输出看作一张连续的“分布倾向图”即可。

图 4:相同范围和色标下,独立随机形成碎点,Perlin 形成连续起伏;叠加多层后出现细节,扭曲坐标后区域形状进一步变化。B—D 调用项目生成器,尚未执行地形分类。
从一层噪声到多尺度区域
只用一层噪声,形状容易过于平滑。项目叠加 5 层不同频率的噪声:每层频率乘 2,振幅乘 0.5,再按总振幅归一化。这种组合称为 fBm。低频决定大片区域,高频补充局部变化。
接着做 Domain Warp(坐标扭曲):先用另外两张低频噪声算出横向、纵向偏移,再到偏移后的位置采样。
warped_position = position + offset(position)
height = fbm(warped_position)
它改变的是采样位置,不是把地图图片事后拉伸。项目的基础海拔尺度为 58 格,扭曲尺度为 70 格,扭曲强度参数为 13 格。尺度越大,区域通常越宽;高频层越多,边缘细节越密。参数应以格子等明确单位记录,避免更换格子大小后地貌跟着变样。
如果直接在 Godot 内生成,FastNoiseLite 已提供 Perlin、fBm 和 Domain Warp。下面是基础 fBm 配置示例;它是便于读者起步的替代实现,不会逐点复现项目的 Python 生成器:
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 官方文档。
噪声提供候选分布,玩法约束决定最终地图
项目分别生成海拔、湿度、风力、森林适生度等场,再组合分类。例如,较高且风力适中的区域成为冻岩,较湿且避风的区域更适合苔地。分开这些场,能避免所有地形都只是同一张噪声的不同颜色条带。
但“低于阈值就是湖”仍可能产生许多小水坑。我们的冰湖先选择相互隔开的低洼湖心,再通过四邻接向外生长,优先扩展到分数较低的位置,并限制湖泊面积和间距。这样可以控制连通性,而不是期待某个随机种子刚好生成好看的湖。
噪声分类之后,还要清理过小区域、保护出生区和洞穴通路,再生成森林与资源。这里的区域清理发生在地图生成阶段;渲染阶段只显示最终地形,不应为了让轮廓更漂亮而偷偷修改地形归属。

图 5:实际生成的地形分布预览。浅色为雪地,亮蓝为冰湖,绿色为苔地,深灰为冻岩,棕色为地热区,外围深蓝为海洋。图中的海岸和地热区还经过额外生成规则,并非单张 Perlin 噪声阈值化的结果。
固定种子方便复现,但种子不是唯一条件。噪声算法、参数、规则顺序改变后,地图仍会改变,所以应同时保存生成器版本和配置。
五、想要不同效果,还可以怎么做?
以上方案适合大面积、连续的手绘地表,并不是所有 2D 游戏都需要自写边界着色器。
| 目标效果 | 可以采用的方案 | 需要付出的代价 |
|---|---|---|
| 像素风草地、平台、墙体,边缘有明确人工造型 | 先规定拼接形状,再制作图块;用 Godot Terrain Peering Bits 自动选块 | 需要补齐邻接情况;引擎不会修复素材本身的接缝 |
| 凹凸边缘丰富的洞穴或实体区域 | Blob47:根据四边及有效对角邻居选块 | 通常需要 47 种非空邻接形态及额外空背景,制作量高于 Corner16 |
| 草逐渐过渡到泥、雪逐渐覆盖岩石 | 用连续权重混合多种材质,再加高度或细节遮罩 | 边缘更柔和,但容易混得发灰;碰撞与通行仍需单独定义 |
| 房间、道路、遗迹等有局部连接规则的结构 | 预制模块配合规则,或使用 WFC 约束相邻模块 | 必须先有合法模块;局部连接正确不代表全图可达,还可能出现求解冲突 |
| 小型固定关卡,希望完全控制构图 | 先定地图布局,再分区绘制,碰撞单独制作 | 构图自由,但扩图、动态开挖和地图复用更费工 |
Godot 的 Terrain 系统支持角、边以及角与边组合匹配;Blob 的有效对角规则可参照前面的 图块方案说明。WFC 的用途和冲突情况见 作者项目说明。它们解决的问题不同,可以组合使用,例如噪声生成自然地貌,预制模块生成遗迹。
无论选择哪条路线,验收都应覆盖大面积重复、凸角与凹角、对角连接、负坐标、区块边界和镜头缩放。静态拼图检查素材;进入 Godot 后再检查采样、坐标和碰撞。
六、把这段要求复制给 AI
使用前补充参考图和项目路径。下面以雪地与冰面两种地形为最小目标,先验证拼接,再扩展其他材质:
请在我的 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 的工作拆成了材质、边界和地图三个可检查的结果。出现问题时,就能针对某一层修改:裂纹断开查材质取样,岸线错位查边界坐标,满地碎斑查分布规则。