Skip to content

在浏览器里造开放世界,第 27 部分:用噪声造一座岛,让地面看起来像地面 ​

作者:Oleg Sidorkin,Cinevva CTO 和联合创始人

刚看到?看系列导览。那里解释了 spike 是什么,并链接了所有部分。

第 26 部分给世界放上了水。这一部分造它底下的陆地,分两个阶段,呼应一个真实的地方是怎么形成的。Spike 54 是地形本身:不是手工雕出来的,而是从噪声里长出来的,然后被风化,直到它有了一个水真正流过的地方该有的水系和海岸线。Spike 55 是那地形上的皮肤:解决那个几乎打败所有程序化地面的问题,就是一张瓷砖纹理看起来就是瓷砖。两者合起来回答一个问题,一个创作者能不能不用美术碰任何一边就拿到一座说得过去的岛和一片读起来像地面的地面,答案是能,只要你抄对了配方。

单靠噪声给不了你的海岸线 ​

在新标签页打开 Spike 54 ↗ · 看源码

高度图照着 Red Blob Games 的配方,那是一摞小变换,每个修原始噪声的一个具体缺陷。分形布朗运动把好几个倍频程的 Perlin 噪声加起来,让地形既有宽阔的丘陵也有细节,但纯 FBM 看起来软乎乎且对齐网格,所以采样点先被域扭曲:你用另一个噪声场偏移每个坐标,p′=p+noise(p)⋅s,把山脊弯成有机的东西而不是锁死在坐标轴上。然后重分布,把海拔升到一个幂 ek,把中间调往下压,让世界有平坦的谷地和尖锐的山峰,而不是所有东西都坐在同样平缓的坡度上。让它成为一座岛而不是无穷丘陵的那一块是一个径向衰减:算一个从中心起的欧几里得距离,d=min(1,nx2+ny2),并把海拔向 1−d 混合,这样地形在边缘落进海里。第二个独立的噪声场成为一张湿度图,它对形状什么也不做,但喂给后面的生物群步骤。

这给了你一个形状,但它是个噪声形状。它没有河、没有水切出的谷、没有沉积扇,因为从来没有东西流过它。修法是水力侵蚀,从 Sebastian Lague 的 MIT 许可实现移植过来。成千上万的水滴随机生成并往下坡滚,每滴带着惯性所以它不做硬的直角转弯,加速时拾起沉积物、减速或积水时沉积下来,配一个预算好的圆形笔刷把每次侵蚀事件铺在一个小半径上,让刻痕平滑而不是一像素的划痕,再加蒸发让水滴在它的生命周期里退役。跑够多水滴,地形就长出噪声造不了的假的东西:会汇聚的水系网络、往下游变宽的谷地,以及沉积物沉下来的平地。

GPU 上的侵蚀,以及知道自己在哪的河 ​

CPU 侵蚀是对的但慢,所以这个 spike 也把它移植到一个 WebGPU 计算 shader,而有意思的部分是 GPU 原子操作逼着你做什么。高度被存成定点 i32,缩放因子一百万,因为 WGSL 没有对浮点的原子加,而成千上万水滴能并行地在同一个 cell 上沉积和移除沉积物而不竞态的唯一办法,是在整数上做 atomicAdd 和 atomicSub。这个定点往返意味着一个 cell 在重并发拖拽下偶尔会轻微变负,这是无害的,回读时被 clamp 掉。CPU 版本预算了一张逐 cell 的笔刷表,但在一个 10012 的网格上那张表大约 100 MB,所以 GPU 移植改成飞速重算每个水滴的笔刷,用一点算术换一大块内存。水滴起始位置仍然通过一个 Mulberry32 生成器从 CPU 来,所以运行是确定性的。

另外两遍把风化过的地形变成一张可读的图。水系用 Red Blob 的 D8 流量累积法:在每个 cell 上滴一单位雨,把所有 cell 按海拔降序排,把每个 cell 累积的水传给它单一的最低邻居,所以水沿着天然的谷地堆起来,任何累积量越过阈值的 cell 都被标成 river。麻烦在于一张侵蚀过的高度图有坑,没有下坡出口的局部洼地,进坑的水就停了,打断累积。所以在水系跑之前,Planchon-Darboux 填坑把每个坑抬到刚好高过它的最低邻居,minNeighbour+ϵ,迭代不到二十遍直到每个 cell 都有地方排水。最后生物群按 Red Blob 那套分配,在海拔对湿度上做一个两轴查表,所以一个又高又干的 cell 成为岩石,而一个又低又湿的成为沼泽,上面叠了坡度感知的覆盖:陡面不管湿度强制成岩石,水线带成为海滩,最高的海拔吃雪。结果是一座你一眼能读懂的岛,端到端从两个噪声种子生成。

停不下来重复的瓷砖 ​

在新标签页打开 Spike 55 ↗ · 看源码

一片地形需要一种材质,用一张只有几米大的纹理覆盖好几公里,而你一把瓷砖放大到覆盖地面,眼睛就锁住了那个重复。Spike 55 是在同一套几何体和光照上对四种采样模式做的 A/B 测试台,所以唯一的变量是纹理怎么被读。基线是 plain:在缩放后的 UV 上采样一次,每隔几米就明显地铺成瓷砖,它存在只是作为被超越的对象。免费的 PBR 层来自 Poly Haven 经它们的 CDN:diffuse 加打包的 ARM 图(环境光遮蔽、roughness、metalness)加一张 GL normal,全部 CC0,用各向异性 8 和 mipmap 加载。

有意思的三种是对重复的不同攻法。hex-linear 模式是 Heitz-Neyret 三角网格:在表面上铺一个倾斜的三角形格子,每个片元坐在一个三角形里,它的三个顶点各取一次随机偏移的采样,按重心权重混合,这样相邻区域采样纹理的不同部分,大尺度的瓷砖化就溶解了。但三个采样的纯线性混合在三角形边缘上平均了它们的颜色,留下一个可见的三角形花纹,正是 spike 48 撞上的那个 artifact。hex-vp 模式是同一篇 2018 论文里发表的修法:在重心求和之后,减去纹理的均值颜色,按平方权重之和的逆平方根重新缩放以保持方差恒定,然后把均值加回去。这让对比度在整个混合里保持稳定,于是三角形消失了,这就是为什么材质会对每张加载的纹理做一次 1×1 的回读,先估出它的均值颜色。第四种模式 iq-untiled 是 Inigo Quilez 的 Texture Repetition 技法 3:两个偏移采样按一个低频变化图案混合,只用两次采样而不是三次,一开始就没有三角形结构,是个站得住的便宜选项。

任何模式之上都可以叠一个可选的三平面投影,这就是让同一种材质能包住一面悬崖而不拉伸的东西。不是一套 UV,shader 采样三个世界轴投影,朝 X 的表面读 YZ 平面,Y 读 ZX,Z 读 XY,并按 abs(normalWorld) 升到一个介于四到八之间的锐化幂混合它们,这样一个 45 度的斜面不会把三个投影都鬼影成一团糊。这个测试台让取舍清清楚楚:保方差的 hex 混合是质量赢家,Quilez 的两采样是讲性能的那个,两者都以足够大的差距胜过 plain,所以任何创作者都不该发布那张 plain 瓷砖。

本章涉及的技术 ​

配方堆叠的高度图生成。 Red Blob Games 的配方把原始噪声组合成地形:用域扭曲 FBM(p′=p+noise(p)⋅s)造有机的山脊,用 ek 重分布造平坦的谷地和尖锐的山峰,以及一个向 1−d 混合的欧几里得径向衰减把边缘落进海里、造出一座岛。第二个噪声场是用于生物群分配的湿度图。见地貌生成。

水力侵蚀,CPU 和 GPU。 Sebastian Lague 的水滴模型(惯性、携带容量、一个预算好的圆形沉积笔刷、蒸发)刻出噪声造不了的真实水系。WebGPU 计算移植把高度存成缩放一百万的定点 i32,这样水滴能并行地在同一个 cell 上做 atomicAdd/atomicSub,飞速重算每个水滴的笔刷以避免在 10012 下那张约 100 MB 的表,并从一个确定性的 Mulberry32 生成器给起始位置播种。

带填坑的 D8 水系。 Red Blob 的 D8 流量累积每个 cell 滴一单位雨,把 cell 按海拔降序排,把水传给每个 cell 的最低邻居,超阈值标成 river。Planchon-Darboux 填坑先把每个局部洼地抬到 minNeighbour+ϵ(迭代不到二十遍),这样水永远不会被困住,累积保持连通。一个海拔对湿度的两轴查表分配生物群,配坡度覆盖在悬崖上强制成岩石、在水线处成海滩、在山峰上成雪。

四种打破纹理瓷砖化的方法。 在同一套几何体和光照上:plain(一次采样,明显铺成瓷砖)、hex-linear(Heitz-Neyret 三角网格,线性混合留下三角形花纹)、hex-vp(EGSR 2018 §3.3 的保方差修法,减去均值、按平方权重的逆平方根重新缩放、再加回均值,每张纹理需要一次 1×1 均值回读)、以及 iq-untiled(Inigo Quilez 的两采样技法 3)。三平面投影采样三个世界轴平面,按 abs(normalWorld) 升到 4-8 次幂混合,这样悬崖不拉伸。保方差在质量上赢,两采样在开销上赢。见地形材质。


第 27 部分,共 29 部分。 上一篇:第 26 部分 - 做水的三种方法 下一篇:第 28 部分 - 一直铺到地平线的草,以及把自己藏起来的地面 系列导览:/zh-CN/blog/2026-02-25-open-world-browser-series-guide