
1. 为什么要做地形侵蚀模拟噪声地形与真实地貌的差距过去很长一段时间里我做地形都是用最省事的办法fBm噪声叠几层加一个域值控制海拔再上个色就完事。孤岛、海岸线、山脉走向确实能看但只要把镜头拉近到山脚或者峡谷拐弯处破绽一下就出来了——坡面太均匀沟壑和冲积扇全是艺术加工出来的伪需求完全经不起“下雨后这里会变成什么样”的推敲。1.1 噪声生成地形的三大硬伤第一是沟壑缺失。真实山体常年被雨水冲刷坡面上应该有很多细小的冲沟山脊线两侧的坡面形态也完全不对称迎风面缓、背风面陡。噪声地形做不出这种有方向性的特征它只有随机高低的“肌理”没有“搬运”的痕迹。第二是沉积区失真。山谷底部、山前平原地带应该是泥沙淤积出来的缓坡和冲积扇噪声地形在这些位置往往还是高低起伏很大视觉上就是山体直接插进了地面中间没有一个缓冲带。第三是坡面角度不合理。自然界岩石和土壤都有自己的息止角超过这个角度土体就会崩塌所以真实的坡面在特定角度区间内会呈现一种“被修正过”的平整感。噪声地形里到处都是不合理的陡坎看久了非常假。1.2 侵蚀模拟到底是什么从“手工雕琢”走向“物理搬运”我当时的想法其实很简单能不能不做“雕山的人”而是做“下雨的平台”——把地形交给一套物理规则去演化让水流自己冲刷出沟壑让重力自己修正坡面。这正是侵蚀模拟的核心思路不直接改高度场而是模拟外力对高度场的改造过程。这个思路落到具体代码上有两条路。一条是经典的无胞自动机/网格迭代方案把地形离散成二维高度场每个格子记录水量、泥沙量、高度反复迭代更新另一条是粒子系统方案用大量粒子代表水滴沿地形流动每个粒子携带水量和泥沙。两条路我都试过网格迭代更适合做实时笔刷粒子系统更细腻但计算开销大后面会详细拆。侵蚀模拟这事做出来以后价值不光是“好看”。它解决了地形生产里的两个长期痛点一是大规模地形的一致性——整张地图的沟谷发育程度、坡面形态能保持统一规律不用人工一格一格去刷二是可复现的交互过程——美术在某个位置刷一下系统按物理规则演化结果是确定的、可预测的不会出现手绘那种每个人风格差很多的情况。2. 水力侵蚀的核心模拟循环降水、搬运与沉积水力侵蚀是整个系统的重头戏也是视觉贡献最大的部分。它的物理过程可以压缩成一句话水带着泥沙从高处往低处流流得动就继续搬运流不动就把泥沙卸下来。这句话翻译成算法就是一套四步循环。2.1 水流与高度场的耦合模型先说明一下我的实现方式。高度场用二维数组存每格存四个量地形高度 (h)、水量 (w)、泥沙量 (s)、以及一个固定的地形类型标记比如岩石、土壤它们的可侵蚀系数不一样。模拟开始前给整张图赋一个初始水量然后每个时间步执行降水按降水速率在随机格子上增加水量流动每个格子的水往周围八个方向中高度差最大的方向流动水量取当前格子的部分比例侵蚀/沉积根据水流速度计算一个“携带能力”如果当前水流能携带的泥沙量大于实际泥沙量就从地形上刮走一部分如果小于就沉积一部分蒸发按蒸发速率减少水量。第3步是核心也是最容易出问题的地方。常见做法是算一个简化版携带能力公式比如[ C k \cdot v \cdot (w_{cell})^\alpha ]其中 (C) 是该格子当前能带走的泥沙量(k) 是侵蚀系数(v) 是水流速度通常取高度差或者流量相关量(w_{cell}) 是本格水量(\alpha) 是经验指数。实际做的过程中我发现这个公式越简单越不容易发散社区里那些花哨的多项式版本参数稍微调偏就会导致地形像被刀刮过一样出现深沟。2.2 关键参数及其物理含义这里把几个关键参数的物理含义和我的经验值列一下参数物理含义典型范围影响降水速率每步新增水量0.0050.02太大则全图积水太小则沟壑发育不足蒸发速率水量消散速度0.0010.01决定流水能流多远值越大水系越短侵蚀系数水流从地表带走泥沙的能力0.010.05决定沟壑深度过大容易穿帮沉积系数泥沙沉降速度0.10.5控制山谷沉积太快会糊成一片携带能力指数水量对侵蚀的非线性影响0.51.5影响沟壑分支形态越大沟越深参数之间的耦合关系比单值更重要。我一开始只调侵蚀系数结果要么沟太深、要么完全没变化。后来把降水速率和蒸发速率一起调才找到感觉降水多了但蒸发快水就来不及走远只在局部形成小沟降水少但蒸发慢水系会拉得很长沟壑很细。这种耦合规律必须靠实测没有捷径。2.3 泥沙搬运的稳定实现泥沙守恒是个容易被忽略的点。如果只在侵蚀上做加法、在沉积上做减法跑几百个迭代之后地形总量会悄悄发生变化甚至出现负高度。我的做法是在同一格子内同时计算侵蚀量和可沉积量取二者与当前泥沙量的最小值确保流动过程不会凭空产生或消灭泥沙。另外一个常见问题是“过量侵蚀”。在水流汇集的谷底水量变大的同时流速也变快携带能力成倍增长一不注意就把整条沟挖到地底。我的处理方式是给侵蚀量加一个上限限制在单格地形高度的某个百分比内比如不超过本格高度的10%。这样每一帧的修改幅度是平滑的不会出现一次迭代就砍掉整座山的情况。3. 热力侵蚀与息止角崩塌带来的山体特征水力侵蚀解决的是“水往低处流”带来的线性搬运但真实地貌还有一个非常重要的塑形力量——重力引发的崩塌。这个机制在大多数地形生成教程里都被一笔带过实际做下来才发现没有它山体会显得非常“脆”坡面上全是违背物理直觉的陡面。3.1 息止角在算法里的作用热力侵蚀的实现逻辑相当朴素遍历每个格子比较它和相邻格子的高度差如果坡度超过某个阈值就把多余的部分从高处挪到低处。这个阈值就是息止角自然界里碎屑堆积体的最大稳定坡度。砂土大概34°碎石能到40°以上岩石则更陡一些。实际做的时候不需要精确到岩性设成35°左右用起来就很舒服。但热力侵蚀不能无限循环做。每做一次相当于把整个地形往“息止角以下”推了一步做多了山体会被磨成毫无起伏的馒头。我自己的经验是热力侵蚀的迭代次数不要超过总迭代次数的20%而且最好与水蚀穿插进行而不是先做完整套水蚀再做热力。3.2 热力侵蚀和水力侵蚀的配合顺序实际操作里两种侵蚀是交替执行的。每跑几轮水蚀就做一两轮热力侵蚀模拟“下雨泡软坡面→崩塌→再下雨搬运”的真实链条。这样产出的地形会有一个很明显的质感坡面是“块状”的有崩塌留下来的小阶梯而不是被水流均匀削过的滑面。配合顺序还会影响沟谷剖面的形态。如果先做大量热力侵蚀再做水蚀地形会先变得圆润水流到时反而难以形成深沟如果水蚀为主、热力为辅沟谷会呈现V字形而且沟壁上有崩塌留下的碎石痕迹——这两种视觉效果在一张真实的地形图上都有对应。我当时还试过把热力侵蚀当成“塑造中景纹理”的手段在大范围地形生成完成后只在特定海拔区间内执行热力侵蚀模拟高山区冻融崩塌带效果出乎意料地好山谷和雪线处的碎石坡一下就立体起来了。4. 把全部迭代过程拆进实时笔刷里全图模拟跑完出一张成品图这对我没有吸引力因为做地形的人希望的是可交互地“画”出地形。所以我把侵蚀模拟从离线迭代改造成了一个笔刷工具——笔刷不是往高度图上加噪声而是在用户鼠标扫过的地方启动一小块物理模拟区域让侵蚀过程在限定范围内连续发生。4.1 笔刷的核心问题定位、强度与衰减笔刷落到实现上有三个关键参数位置、强度和衰减。位置好理解鼠标点在哪个格子上就从哪个格子触发模拟强度是指这个区域内降水速率和侵蚀系数的组合强度越大局部沟谷越深衰减则是指从笔刷中心到边缘的参数渐变避免出现“刷出一道切割线”的生硬过渡。我用的是双高斯衰减强度从笔刷中心向外逐步降低同时侵蚀系数的衰减比降水速率略慢一点。这样做的好处是沟壑在中心位置最深向边缘逐渐变浅整体过渡比较自然不会留下明显的笔触边界。4.2 交互式侵蚀的循环逻辑及现场更新笔刷触发之后系统会把笔刷覆盖的区域当作一个微型高度场在其中跑前面说的四步循环。这里有一个很关键的设计决策笔刷区域内部的模拟必须持续足够多的迭代才能在同一次交互中看到沟壑成型。如果每帧只跑一步用户要按住很久才能看到变化交互感会很差。我的做法是滑动窗口批量迭代。鼠标拖动过程中每次只跑核心区域的小范围迭代鼠标停下来按下不动时就自动打包跑50100次迭代让沟壑在12秒内“长”出来。配合进度画刷和强度反馈用户能明显感觉到模拟正在发生而不是单纯地在改一个参数。4.3 为什么要做“笔刷”而不是“全图模拟”全图模拟的问题不只是速度还在于太均匀。同一张地图上不同位置的降水强度、植被覆盖、岩性都是不同的全图跑同一套参数出来的地形细节和沟壑密度全球统一非常不真实。笔刷方案允许我只在特定流域比如一片汇水区增强侵蚀模拟而在冲积平原区完全关掉侵蚀这样地形不同区域的发育程度就有了差异这是真实地貌的根本特征。还有一层实用理由美术的创作习惯本来就是局部迭代的。全图模拟一旦开了头改起来非常痛苦跑错了只能重来。笔刷的方式则天然支持“哪里不满意就刷哪里”配合完善的重做栈整个制作流程更可控。5. 性能优化与模拟稳定性实践地形侵蚀模拟是个典型的计算密集型任务实时笔刷更是对性能苛刻。我这套系统跑了两版第一版纯CPU单线程1024分辨率的地形笔刷区域迭代50次大约要1.2秒勉强能用但卡顿感明显第二版改成了多线程分块迭代速度上来了但引入了一堆同步bug。这里分享几个实测下来的关键优化点。5.1 数据布局与线程切分方式高度场、水量、泥沙量这三个二维数组是最大宗的数据。我一开始按普通二维数组存储每个格子把三块数据分开访问缓存命中率很差。后来改成结构体数组SoA布局把水量和泥沙量按通道存储再叠加上多线程分块性能提升非常明显。多线程切分要注意的是边界同步水蚀过程中一个格子要读取周围8个格子的数据。分块线程如果不共享边界块与块之间会出现撕裂感。我的方案是每个线程块在自己的边界上多读一圈数据也就是Ghost Cell技术算完之后再做一次边界同步。这样做之后拼接处的确看不出任何不连续。5.2 迭代到收敛的稳定性控制物理模拟跑太长会出问题最常见的是“无限侵蚀”——某个区域水量一直很大携带能力一直比泥沙量大导致沟壑无限加深。解决的办法有两个一个是上一节说的侵蚀上限另一个是引入基准面限制。设定一个全局最低海拔当某格高度逼近基准面时侵蚀系数自动衰减到原来的10%左右模拟自然停在一个合理深度。另一个稳定性问题是“脉冲振荡”。某些情况下水量和泥沙量会在相邻格子间来回震荡地形高度也会出现周期性的起伏。排查后发现是迭代步长和流动比例的参数乘积超过了稳定性条件。我的做法是用局部步长自适应每个格子根据当前水量与周围高度差动态调整流动比例这样既保证了整体收敛也不会出现过冲。做实时笔刷时还要注意一点不要把模拟的每一步都写进可编辑的高度层。先把模拟结果缓存到一个临时缓冲区等用户松开鼠标按键再把最终效果合并到主高度场上。这样避免了半成品状态的闪烁和回退困难合并时还能做一次平滑过渡视觉上更稳。6. 总结我的经验与建议整套系统从想法到能稳定出图前后花了两周半。期间踩的坑不少最深的几个——过量侵蚀、边界撕裂、迭代不收敛——在上文都讲到了。如果你也想做类似的东西我最后再给几条实操层面的建议。从最简单的水蚀模型开始不要一步登天。我先实现的是单层水蚀没有热侵蚀也没有粒子系统就是网格迭代四步循环。把这套跑通并理解“参数耦合”的脾气再往上加东西效率远高于一上来就堆整套物理模型。参数调节要有记录习惯。侵蚀模拟的参数空间非常大几个参数之间互相影响随手调很容易迷失。我用的是最土的办法每个参数组合命名存档记录下来地形输出的特征差异和迭代结果截图。半个月下来这组记录成了我判断任意新地形参数的最重要参考。视觉验证要分层。只看最终高度图很难判断是水蚀参数问题还是热力配合问题。我的做法是调试图时并行输出高度图、水量图、泥沙图三通道一眼就能看出哪个环节异常。这个做法在排查“峡谷过深但沉积区过平”这类问题时帮了很大的忙。收录进工具链比做独立工具更实际。这身流体处理能力最终要用在正式流程里。我的做法是把它封装成一套API供地形生成器和场景编辑器共同调用。做地形时可以直接刷、可以预计算整张图、也可以后期局部迭代修改这比做一个“看起来壮观但只能单独玩”的技术演示要实在地多。侵蚀笔刷这套技术我理解下来本质上不是在教你画地形而是在教地形自己长出来。虽然模拟的物理过程经过了大量简化但只要你把“搬运”“崩塌”“沉积”这三件事的先后和力度关系处理对出来的东西天然就带有自然界的逻辑感。这一点是任何手工雕刻和噪声叠加都替代不了的。