想象你是某个大型游乐园的景观设计师。老板丢过来一张 100 平方公里的规划图,说:“周五之前,我要看到完整的山、河、森林,但只给你三个人的团队。”手工去种每一棵树、雕每一座山显然不可能。聪明的设计师决定换一种思路:不亲自雕刻地形,而是设计“地形自己长出来”的规则。先播下一片“随机但连续”的种子——风一吹,草浪自然起伏;再叠上几层不同尺度的波浪,小山包上长出岩石棱角和沙粒粗糙;接着下一场“模拟雨”,让水流顺着山坡刻出沟壑;最后按海拔铺上草皮、岩石和积雪。整个过程,设计师只写了几个函数和规则,一座像模像样的山野花园就诞生了。这就是 PCG 程序化地形生成:用算法代替手工,用规则代替日复一日的重复劳动。接下来,我们从最基础的一颗“随机种子”——Perlin 噪声讲起,一步步种出属于你的无限世界。

岗位关联

掌握程序化地形生成,你可以用几百行代码“种”出别人几周手绘的开放世界。这是游戏开发、数字孪生和 AI 内容生产领域的底层硬技能,也是技术美术和引擎开发岗位的核心加分项。

目录
  1. 可视化演示
  2. 噪声函数基础
  3. 分形叠加FBM
  4. 高度图生成
  5. 侵蚀效果模拟
  6. 细节丰富手段
  7. 性能优化策略
  8. 地形实战应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

高度图 (Heightmap) 深海 浅海 海岸 沙滩 草原 山地 雪峰 fBm: 生物群落图 (Biome Map) total = sum(noise(freq*2^i) * amp*0.5^i) / maxVal, i=0..octaves 侵蚀速率 = (高度差/距...

噪声函数基础

假设你在花园里撒了一把草籽,风一吹,草浪齐刷刷地起伏。仔细观察会发现一个规律:每一株草的摆动方向都不同,但相邻的两株草绝不会一棵向左倒、一棵向右倒——它们像被同一阵风推着,呈现出“随机但连续”的波浪。普通随机数做不到这一点,它更像同时被八个方向的风乱吹,画面是一团电视雪花。地形生成需要的第一件工具,正是这种“随机但连续”的波动。

Perlin 噪声(Perlin Noise)就是游戏程序里那阵“温柔的风”。它由 Ken Perlin 在 1985 年为电影特效提出,核心思路是:把空间切成一个个整数格子(晶格),每个格点(整数位置)上放一个随机高度,格子内部的高度则用一条平滑曲线从两个端点“插”出来。这样得到的曲线处处连续,没有生硬的折角,放大缩小都保留自然起伏。

一维 Perlin 噪声的计算分两步:先找到采样点所在的格子,取出左右两个格点的随机值;再用一个平滑后的权重把这两个值混合。关键在于权重不能直接用线性比例,而要经过平滑函数 s(t) 处理——它让两个端点附近的变化率都为 0,曲线像被轻轻“摁”过一样圆润。

有了这阵“风”,花园就有了第一层底子:连绵的草浪。但只有一层风,地形太单调——就像只有一片缓坡草地,看不出山和谷。要让地貌丰富起来,我们需要把多阵不同强弱的风叠在一起。

f(x)=a⋅(1−s(t))+b⋅s(t),s(t)=6t5−15t4+10t3\displaystyle f(x) = a \cdot \bigl(1 - s(t)\bigr) + b \cdot s(t), \qquad s(t) = 6t^5 - 15t^4 + 10t^3
公式解读:f(x) 是当前采样点的高度,a 和 b 是左右两个格点的随机值,t 是采样点在格内的相对位置。s(t) 把线性比例 t 改成 S 形曲线,让两段接得更自然。
  1. 格内位置 t计算采样点 x 在自己的晶格内走了多少比例,t 始终在 0 到 1 之间。
    t=x−⌊x⌋t = x - \lfloor x \rfloor
  2. 端点随机值左右两个整数格点各自持有的随机高度,它们是插值的两个端点,相当于草浪两端的基准。
    a=noise(⌊x⌋),b=noise(⌊x⌋+1)a = \text{noise}(\lfloor x \rfloor), \quad b = \text{noise}(\lfloor x \rfloor + 1)
  3. 平滑权重用 5 次多项式把 t 从直线变成 S 形,保证端点处斜率为 0,曲线不出现折角。
    s(t)=6t5−15t4+10t3s(t) = 6t^5 - 15t^4 + 10t^3
  4. 加权混合a 以 1 - s(t) 的权重“退场”,b 以 s(t) 的权重“进场”,合成为连续值。
    f(x)=a⋅(1−s(t))+b⋅s(t)f(x) = a \cdot (1 - s(t)) + b \cdot s(t)

分形叠加FBM

真实的山脉站在远处看,轮廓是大尺度的起伏;走近看,山体表面有岩石的棱角;再贴近,还有砂砾的粗糙。单独一层 Perlin 噪声只能提供其中一种尺度,就像花园里只有一阵风,吹出大同小异的波浪。自然界的地形几乎都是“自相似”的:放大看和缩小看都有细节,这种结构叫作分形。

分形布朗运动(fBm,fractal Brownian motion)的思想很简单:把多层噪声像叠罗汉一样叠起来。第一层用低频、大幅值,负责山体的大骨架;第二层频率翻倍、振幅减半,负责岩石棱角;第三层再翻倍、再减半,负责沙粒细节。层数越多,地形越接近“处处有细节”的自然地貌。

每一层叫作一个八度(octave)。设第 i 层振幅为 \(A_i\)、频率为 \(F_i\),相邻层由两个常数控制:persistence(振幅保持率,常用 0.5,控制起伏衰减速度)和 lacunarity(频率增长率,常用 2.0,控制细节加密速度)。把各层采样结果求和,就得到一张既有大山又有碎石的地形基底。

花园里同时吹起四阵风:大风推大浪、中风中浪、小风小浪,叠在一起,原本单调的草浪就变成了有山有谷、层次分明的荒野——这正是可视化演示里“第 1 层到第 4 层”叠加出的效果。但美景当前,我们手里还只有一堆“相对高度”,要把它变成能施工的图纸,还需要一张标准化地图。

fBm(x)=∑i=0n−1Ai⋅noise(Fi⋅x+ϕi),Ai+1=p⋅Ai,Fi+1=l⋅Fi\displaystyle \text{fBm}(x) = \sum_{i=0}^{n-1} A_i \cdot \text{noise}\bigl(F_i \cdot x + \phi_i\bigr), \qquad A_{i+1} = p \cdot A_i, \quad F_{i+1} = l \cdot F_i
公式解读:fBm 就是把 n 层不同频率、不同振幅的噪声加权求和。p 是振幅衰减系数(persistence),l 是频率增长系数(lacunarity),\(\phi_i\) 是各层的相位偏移。
  1. 求和范围n 是八度总数,i 从 0 到 n-1 逐层累加,每一项代表一层独立的噪声。
    ∑i=0n−1\sum_{i=0}^{n-1}
  2. 振幅衰减每层振幅乘上 persistence p(通常 0.5),层数越深,对整体的贡献越小。
    Ai+1=p⋅AiA_{i+1} = p \cdot A_i
  3. 频率倍增每层频率乘上 lacunarity l(通常 2.0),深层噪声的波形越来越密,负责细小纹理。
    Fi+1=l⋅FiF_{i+1} = l \cdot F_i
  4. 相位错开给每层加一个随机相位偏移,避免所有层的波峰波谷重叠出规律条纹。
    ϕi=随机常数\phi_i = \text{随机常数}

高度图生成

施工队进场前,设计师需要一张能指导作业的图纸。PCG 地形的“图纸”叫高度图(Heightmap):一张二维数组,每个格子记录对应位置的海拔数值。它不关心颜色和材质,只回答一个问题——这个点是低洼盆地还是高耸山峰。高度图是地形系统与渲染引擎之间的通用语言。

把 fBm 铺满整个二维平面后,每个点 \((x,z)\) 都会得到一个原始噪声值。问题在于,这个值通常散布在某个不确定的范围(比如 -0.8 到 0.7),而我们想要的是“海平面以下 50 米到雪线 800 米”这种明确的海拔。直接拿噪声值当地形高度,范围对不上、均值也对不齐。

解决办法是归一化映射:先统计整张图的最小值 \(h_{\min}\) 和最大值 \(h_{\max}\),把每个点的值压成 0 到 1 之间的比例,再把这个比例填充到目标高度区间 \([H_{\min}, H_{\max}]\)。之后按高度做阈值分层:低于海平面是水底,中间是沙滩和草甸,高处是裸岩和积雪。

图纸画好了:低洼处标着“未来湖泊”,平缓处标着“草甸”,高处标着“雪峰”。但这张图还太“光滑”,像一块被熨平的布料——真实世界的地形,还需要被水流冲刷出沟壑。下一节,我们给花园下一场“数字雨”。

H(x,z)=Hmin⁡+fBm(x,z)−hmin⁡hmax⁡−hmin⁡⋅(Hmax⁡−Hmin⁡)\displaystyle H(x,z) = H_{\min} + \frac{\text{fBm}(x,z) - h_{\min}}{h_{\max} - h_{\min}} \cdot (H_{\max} - H_{\min})
公式解读:\(\text{fBm}(x,z)\) 是噪声原始值,分子算出该点在整个噪声范围内的位置比例,再乘到目标海拔范围上。这样无论噪声输出什么范围,地形都稳定落在设定的海拔带里。
  1. 原始噪声值用平面坐标 \((x,z)\) 输入 fBm,得到该点的地形“毛坯”高度。
    raw=fBm(x,z)\text{raw} = \text{fBm}(x,z)
  2. 统计极值遍历整张图,找噪声输出的最小值和最大值,作为归一化的标尺。
    hmin⁡=min⁡x,zfBm(x,z),hmax⁡=max⁡x,zfBm(x,z)h_{\min} = \min_{x,z} \text{fBm}(x,z), \quad h_{\max} = \max_{x,z} \text{fBm}(x,z)
  3. 压缩成比例把 raw 减去最小值、除以极差,得到 0 到 1 之间的比例 r。
    r=raw−hmin⁡hmax⁡−hmin⁡r = \frac{\text{raw} - h_{\min}}{h_{\max} - h_{\min}}
  4. 映射海拔带用比例 r 在目标最低海拔与最高海拔之间线性插值,得到最终高度。
    H(x,z)=Hmin⁡+r⋅(Hmax⁡−Hmin⁡)H(x,z) = H_{\min} + r \cdot (H_{\max} - H_{\min})

侵蚀效果模拟

现实中的山不是天生圆润的:雨水顺着山坡流下,把高处松动的泥土一点点搬到低处,日积月累,山体上刻出一道道沟壑,山脚下堆出冲积扇。如果我们也给地形“下一场数字雨”,就能用算法替大自然赶工,省去美术手工雕沟的苦力。这正是侵蚀模拟(erosion)的魅力。

最常用的算法是粒子侵蚀(particle-based hydraulic erosion)。想象无数颗小水滴在高度图上滚动:水滴沿着最陡的下降方向移动,速度越快,能携带的泥沙越多;当它流到平缓处速度变慢,装不下的泥沙就沉积下来。每颗水滴走过一条路径,都在悄悄改变地形——被刮走泥土的地方变低变陡,泥沙沉积的地方逐渐堆高。

数学上,每颗水滴在一次移动中要算三件事:先由地形高度差求出速度,再由速度算出它的携沙容量 \(C_{\text{cap}}\);然后把当前携带量 \(s_{\text{cur}}\) 与容量比较——容量富余就继续侵蚀刮土,携带超量就放下泥沙;最后把高度变化写回地形。对整张图迭代数千颗水滴,沟壑、河谷、冲积扇就都“长”了出来。

数字雨下完之后,花园从“熨平的布料”变成了“被溪流雕刻过的山谷”——沟壑的走向和冲积扇的形状都是物理规则自己算出来的,带着手工永远画不出的自然随机感。不过此时山脊线还略“顺滑”,下一步我们要把图纸揉一揉,揉出更野性的曲线。

Ccap=kc⋅∥v∥,Δh=ks⋅(Ccap−scur)\displaystyle C_{\text{cap}} = k_c \cdot \|v\|, \qquad \Delta h = k_s \cdot \bigl(C_{\text{cap}} - s_{\text{cur}}\bigr)
公式解读:\(v\) 是水滴流速,\(C_{\text{cap}}\) 是它能背负的泥沙上限,正比于流速。\(s_{\text{cur}}\) 是当前携带量,差值为正时沉积泥沙(\(\Delta h > 0\)),为负时侵蚀地表(\(\Delta h < 0\))。
  1. 流速计算水滴沿最陡下降方向移动,速度与高度差成正比,坡度越陡流得越快。
    v=kd⋅(hcur−hnext)v = k_d \cdot (h_{\text{cur}} - h_{\text{next}})
  2. 携沙容量流速越快,水能托起的泥沙越多,容量与速度大小成正比。
    Ccap=kc⋅∥v∥C_{\text{cap}} = k_c \cdot \|v\|
  3. 容量差值容量和当前携带量的差值决定下一步是“吃土”还是“吐土”。
    Δs=Ccap−scur\Delta s = C_{\text{cap}} - s_{\text{cur}}
  4. 高度变化差值乘以沉积系数 \(k_s\),正数让地面变高(沉积)、负数让地面变低(侵蚀)。
    Δh=ks⋅Δs\Delta h = k_s \cdot \Delta s

细节丰富手段

一张纸摊在桌上时,上面的线是平直的;把它揉成一团再展开,直线全变成了蜿蜒的曲线,而且每道褶皱之间还藏着微妙的关系。域扭曲(domain warping)就是地形的“揉纸术”——它不直接改高度,而是先“揉”坐标,再让地形在揉皱的空间里生长。

常规做法是直接采样 \(\text{fBm}(p)\) 得到高度。域扭曲在采样之前先动手脚:对坐标 p 先做一次噪声扰动,得到 \(p' = p + k \cdot \text{noise}(p)\),再拿着 \(p'\) 去采样 fBm。原本规整的山脊线会被“拉弯”,形成更纠缠、更自然的地貌,特别适合模拟悬崖、岩洞和蜿蜒的河道。

另一个常用的“抠细节”手段是阶梯地形(terraces,梯田):对高度做量化取整,让平滑山坡变成一级一级的台阶,适合梯田、矿山和砂岩地层。除此之外,还可以把多种噪声按权重混合(多噪声混合),例如 70% 的 fBm 叠加 30% 的细胞噪声,让地形同时具备山脉绵延和岩石碎裂两种气质。

揉过纸的地图,山脊不再是一条平滑弧线,而是带着弯折与回环的“手写笔迹”,视觉上已经很接近真实世界。但别忘了,我们还不知道手机和电脑受不受得了——下一节,我们解决“大地图如何流畅运行”的问题。

p′=p+kwarp⋅noise(p)\displaystyle p' = p + k_{\text{warp}} \cdot \text{noise}(p)
公式解读:p 是原始采样坐标,\(\text{noise}(p)\) 是同一位置的噪声值,\(k_{\text{warp}}\) 控制揉皱强度。扭曲后的坐标 p' 再送去采样 fBm,地形就像被揉过的纸一样蜿蜒起来。
  1. 原始坐标当前要采样的平面位置,可以是一个二维点。
    p=(x,z)p = (x, z)
  2. 扰动噪声在原坐标处取一个噪声值,作为偏移的方向和大小。
    n=noise(p)n = \text{noise}(p)
  3. 扰动强度\(k_{\text{warp}}\) 越大,坐标被推得越远,地形越“皱”。
    kwarp⋅nk_{\text{warp}} \cdot n
  4. 扭曲后采样用揉皱的新坐标去取高度,原本笔直的山脊被拉成回环曲线。
    height=fBm(p′)\text{height} = \text{fBm}(p')

性能优化策略

游客坐观光车进入花园:远处的山只能看到整体轮廓,车开近了,石头的纹理、花瓣的脉络才逐层显现。如果园方在每种距离都把每一片花瓣渲染出来,再大的服务器也会崩溃。地形渲染同理——远处的网格根本不需要细分到极致。

LOD(Level of Detail,细节层次)的核心思想是“按距离调整网格精度”:摄像机近处用密密麻麻的小三角形,远处用稀疏的大三角形。远处的大三角形投影到屏幕上本来就只占几个像素,肉眼几乎分辨不出差别,却能省下海量顶点开销。细节级别随距离呈对数增长:距离每翻一倍,级别降一档。

工程上,地形还要切成一个个方块(chunk),只生成视野内的块,远离的块卸载或降级;已经生成过的块放进缓存复用——因为同一个坐标 + 同一个种子永远算出同一个高度,玩家绕一圈回来不会看到“地形变了”。

观光车的视角下,近处草叶分明、远处群山如黛,内存和帧率都很稳。有了“远近有别”的策略,一座 16 平方公里的开放世界才能在普通电脑上跑起来。最后一步:图纸、沟壑和性能都搞定后,正式对外开放景区。

LOD(d)=min⁡(⌊log⁡2(d/d0)⌋, Lmax⁡)\displaystyle \text{LOD}(d) = \min\Bigl(\bigl\lfloor \log_2(d / d_0) \bigr\rfloor, \, L_{\max}\Bigr)
公式解读:d 是地块到摄像机的距离,\(d_0\) 是基准距离。距离每翻一倍,\(\log_2\) 值加 1,细节就降一级,最后用 \(L_{\max}\) 限制下限,防止细节无限变粗。
  1. 距离计算求出地块中心(或顶点)到摄像机位置的距离,作为分级依据。
    d=∥Pchunk−Pcamera∥d = \|P_{\text{chunk}} - P_{\text{camera}}\|
  2. 基准距离\(d_0\) 是精度切换的参考尺度,比如 100 米内用最高精度。
    d0=常量d_0 = \text{常量}
  3. 对数降级距离除以基准后取 2 的对数,距离翻倍级别加一,再向下取整。
    ⌊log⁡2(d/d0)⌋\lfloor \log_2(d / d_0) \rfloor
  4. 上限裁剪用 \(L_{\max}\) 约束最大 LOD 级别,防止极端远处网格粗糙到无法接受。
    Lmax⁡L_{\max}

地形实战应用

图纸完工、雨水雕琢完毕,景区要开张了:给不同海拔铺上不同地表——湖边是沙滩和草地,山腰是碎石和灌木,雪线以上是皑皑白雪。这一步叫纹理混合(texture blending):不是简单贴一张图,而是按高度和坡度给每种地表算权重,再按权重把颜色混合起来,让草地到岩石之间出现自然的过渡带。

除了地表颜色,同一张高度图还能驱动生态散布:噪声值高的背阴坡种杉树,低洼向阳处铺草皮、放石头;建筑和道路的选址也可以参考高度与坡度。最终所有数据导出成网格,交给游戏引擎做渲染和碰撞检测。一个典型工作流是:设计一个“传入坐标与种子就返回高度”的地形函数,在地形编辑器中调好参数,离线烘焙高度图与法线图,运行时按 chunk 流式加载。

这样设计还有一个隐藏好处——可复现性:同一个种子能复现同一座山,适合做存档共享、多人联机同步和自动化测试。玩家保存时只需要记录种子和修改过的地块,存档体积能压缩几个数量级。

至此,一座用几百行算法“种”出来的山野花园正式开放:有海有滩、有谷有壑、有树有雪,还能在任意设备上流畅游览。你不需要手工雕刻每座山,只需要写好几阵“风”、一场“雨”和一张“图纸”——这就是 PCG 程序化地形生成的全部秘密。

cfinal=∑i=1nwi⋅ci,wi=wi(h)⋅wi(s)\displaystyle c_{\text{final}} = \sum_{i=1}^{n} w_i \cdot c_i, \qquad w_i = w_i^{(h)} \cdot w_i^{(s)}
公式解读:最终地表颜色是 n 层纹理的加权平均。每层权重由高度权重 \(w_i^{(h)}\) 和坡度权重 \(w_i^{(s)}\) 相乘得到,例如“草地”在低海拔缓坡权重高,“岩石”在陡坡权重高。
  1. 纹理层集合草地、泥土、岩石、积雪等 n 种地表贴图,各是一个候选颜色。
    {c1,c2,…,cn}\{c_1, c_2, \dots, c_n\}
  2. 高度权重根据海拔在预设区间内的位置,决定每种地表的基础权重。
    wi(h)=ramp(H,lowi,highi)w_i^{(h)} = \text{ramp}(H, \text{low}_i, \text{high}_i)
  3. 坡度权重用坡度(高度变化率)修正权重,陡峭处更多露出岩石。
    wi(s)=f(slope(x,z))w_i^{(s)} = f(\text{slope}(x,z))
  4. 加权合成把所有层按归一化权重混合,得到平滑过渡的最终颜色。
    cfinal=∑i=1nwi⋅cic_{\text{final}} = \sum_{i=1}^{n} w_i \cdot c_i

AI 应用场景

开放世界大地图快速预生成

阶段:内容生成(程序化资产创建)

解决的问题:开放世界地图若全部手工搭建,一个 4 平方公里的区域往往要美术团队数周时间,且地块间风格难以统一。用 Perlin 噪声 + fBm 生成基础高度图,再用水力侵蚀增加沟壑细节,几分钟内就能产出覆盖全图的地形初稿,美术只需在关键区域做手工精修——就像园丁先撒种子让草自己长,再修剪出造型。

具体案例:某开放世界项目用固定种子生成 256×256 个地形块,每块 128×128 个顶点,PCG 管线在启动时一次性算出完整大陆,随后美术只在“圣城、峡谷、海岸”三个关键区域手工细化,整体工期缩短约 70%。

生态植被与地物自动散布

阶段:关卡与生态合成

解决的问题:人工在数万棵树木和岩石之间逐个摆放位置,既重复又缺乏自然聚类。用高度图加噪声做生态映射:高度低且坡度小的区域长草,陡坡生灌木,高海拔种针叶林,河流沿线留出空地——规则自动算出每个物体的位置、旋转和缩放,就像按图纸给花园“批量种树”。

具体案例:某 3D 游戏在预烘焙阶段,PCG 脚本读取高度图和坡度图,1 分钟内在 1 平方公里的场景中散布了 12 万棵树、3 万块石头,同时保证道路和建筑区域完全避让,运行帧率如常。

超大地图流式加载与存档复现

阶段:运行时性能优化

解决的问题:大地图若一次加载全部网格,内存会立刻爆炸。按 chunk 切分 + LOD 分级 + 同种子缓存后,只有视野内的块被实例化,远处块动态卸载。玩家保存时只记录种子和修改过的地块,存档体积从几百 MB 降到几 KB——游客逛花园时,园丁只把眼前的路修好,远处留白即可。

具体案例:某沙盒游戏在联机版本中,每个客户端只维护周围 5×5 个 chunk,服务器下发种子后客户端本地重建地形,配合共享的修改哈希表,实现了跨设备一致的地形复现。

Python 演示

python59 行
import math  def hash2(x, y, seed):    return math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453 % 1.0  def smooth(t):    return t * t * (3 - 2 * t)  def value_noise2(x, y, seed):    xi, yi = int(x), int(y)    xf, yf = x - xi, y - yi    a = hash2(xi, yi, seed)    b = hash2(xi + 1, yi, seed)    c = hash2(xi, yi + 1, seed)    d = hash2(xi + 1, yi + 1, seed)    ux, uy = smooth(xf), smooth(yf)    top = a + (b - a) * ux    bot = c + (d - c) * ux    return top + (bot - top) * uy  def fbm2(x, y, octaves, seed):    total, amp, freq, amp_sum = 0.0, 1.0, 1.0, 0.0    for i in range(octaves):        total += value_noise2(x * freq, y * freq, seed + i * 101) * amp        amp_sum += amp        amp *= 0.5        freq *= 2.0    return total / amp_sum  def generate_heightmap(width, height, seed, octaves=6):    hm = []    for i in range(height):        row = []        for j in range(width):            row.append(fbm2(j / width * 6.0, i / height * 6.0, octaves, seed))        hm.append(row)    return hm  def print_terrain(hm):    symbols = " .:*#@"    flat = [v for row in hm for v in row]    min_v, max_v = min(flat), max(flat)    for row in hm:        line = "".join(            symbols[int((v - min_v) / (max_v - min_v + 1e-9) * (len(symbols) - 1))]            for v in row        )        print(line)  if __name__ == "__main__":    terrain = generate_heightmap(64, 24, seed=42, octaves=6)    print_terrain(terrain)
目的:用约 50 行代码实现二维值噪声和 fBm,生成一张 ASCII 地形图——跑一遍就能在终端看到“海边、平原、山地”的自然渐变,是 PCG 地形核心算法的完整最小实现。
  1. def value_noise2(x, y, seed):二维噪声核心:每个整数格点有随机值,格内做平滑插值,产生“随机但连续”的地形基底,对应正文里的“草浪种子”。
  2. top = a + (b - a) * ux先在 x 方向做平滑插值,得到格子上下两条边的中间值,这是双线性插值的第一步。
  3. return top + (bot - top) * uy再在 y 方向插值一次,两趟插值把四个角点合成一个平滑曲面。
  4. total += value_noise2(x * freq, y * freq, seed + i * 101) * amp每个八度按当前频率采样、按振幅加权累加,这就是 fBm 的“叠罗汉”核心。
  5. amp_sum += amp累计所有八度的振幅之和,作为归一化分母,保证结果落在 0 到 1 之间。
  6. symbols = " .:*#@"用 6 级 ASCII 字符表示高度,从空格(低地)到 @(雪峰),方便在终端直接读地形。
  7. terrain = generate_heightmap(64, 24, seed=42, octaves=6)一行代码生成 64×24 的地形,种子固定就能复现同一座山,对应“可复现性”工程实践。

常见误区

误区“生成地形嘛,直接 random() 给每个点随机一个高度不就行了?反正都是程序自动生成的,效果差不多。”

正解random() 每个点完全独立,生成的是“电视雪花”而不是山。Perlin 噪声的价值恰恰在“随机但连续”——相邻点高度相关,像同一阵风吹动的草浪。自然地形是空间相关的,这是它与白噪声的本质区别。用随机数铺出来的“地形”没有山脊、山谷和坡度,完全无法通过视觉验收。

为何常见:初学者容易把“程序化生成”等同于“随机”,忽略了地形最重要的空间连续性,直到渲染出来才发现是一团噪点。

误区“fBm 叠加的层数越多地形越真实,所以我要叠 30 层,细节永不嫌多。”

正解振幅按 persistence=0.5 指数衰减,10 层之后每层的贡献已经不足千分之一,肉眼完全看不出差别,纯属浪费算力。实践中 4~8 层足够,更高的“视觉细节”应该交给域扭曲和侵蚀模拟去塑造——它们改变地形结构的方式,比加噪声层有效率得多。

为何常见:只看到“层数多=细节多”的直觉,没有意识到振幅衰减是指数级的,也没有区分“噪声细节”和“结构细节”。

误区“侵蚀模拟不过是在噪声地形上再加点随机扰动,让它更好看,跟加个滤镜差不多。”

正解侵蚀是真实的物理输运过程:水沿坡度流动、携带泥沙、按容量沉积,把高处的土搬到低处并刻出沟壑。它是流程性算法,会根本改变地形的等高线、河谷和冲积扇形态,而不是在表面的颜色上加噪点。滤镜改变的是“看起来”,侵蚀改变的是“地形本身”。

为何常见:只看了最终渲染图,误以为沟壑是后处理效果,没意识到高度数据已经被粒子逐格修改。

误区“地图多大就生成多大,反正生成很快,直接全部生成完存进内存,一了百了。”

正解大型开放世界地图常达到 16 平方公里,网格顶点数以亿计,一次生成会撑爆内存、拖垮加载。工程实践用分块 + LOD + 缓存:只生成视野内的块、远处降级、同种子复算缓存块,内存和帧率才能稳定。就像花园导游车永远只照亮眼前的路,而不是给整座山装灯。

为何常见:没有接触过超大规模场景,低估了顶点数和内存开销的爆炸式增长,只在 Demo 级别的小地图上验证过方案。

面试题

Q1. Perlin 噪声与普通随机数最核心的区别是什么?

单选题 · easy

  • A.完全独立的随机输出值
  • B.输出连续且空间相关← 正确答案
  • C.输出结果一定是整数
  • D.只能处理一维的数据

答案:B。Perlin 噪声的价值在于“随机但连续”——像花园里被同一阵风推动的草浪,相邻位置高度相关;而普通随机数每个点完全独立,像八面来风的电视雪花,无法模拟自然山体。C、D 显然错误,A 描述的正是随机数的缺陷。

Q2. fBm 分形叠加中,新增八度的振幅通常如何变化?

单选题 · easy

  • A.每次翻倍变得越来越大
  • B.每次减半变得越来越小← 正确答案
  • C.完全随机变化没有规律
  • D.与频率没有任何关系

答案:B。fBm 中每层振幅乘 persistence(通常 0.5)递减,对应“大浪推小浪”的直觉——层数越深贡献越小。若振幅翻倍,地形会被高频噪声淹没,无法保持低层的大尺度山体轮廓。

Q3. 高度图本质上是什么数据结构?

单选题 · easy

  • A.存储颜色的三维矩阵
  • B.记录海拔的二维数组← 正确答案
  • C.保存声音的一维序列
  • D.描述模型的顶点索引

答案:B。高度图就是一张二维数组,每个格子记录对应位置的海拔,像花园施工图纸上每个点的高度标注。它不存颜色、不存声音、不存顶点连接关系,只回答“这里多高”这一个问题。

Q4. 水力侵蚀中,粒子能携带的沉积物上限与什么有关?

单选题 · mid

  • A.粒子的移动速度大小← 正确答案
  • B.粒子的初始发射数量
  • C.地形图的整体尺寸大小
  • D.噪声函数的随机种子

答案:A。携沙容量公式 \(C_{\text{cap}} = k_c \cdot \|v\|\) 表明它与流速成正比,就像水流越急能托起的泥沙越多。流速下降时容量变小,超出的泥沙沉积下来,这与粒子数量、地图尺寸和随机种子无关。

Q5. 域扭曲(domain warping)的核心做法是什么?

单选题 · mid

  • A.先扭曲坐标再采样噪声← 正确答案
  • B.直接修改噪声的输出值
  • C.减少噪声叠加的层数
  • D.把噪声值强制转成整数

答案:A。域扭曲是对采样坐标先做扰动 \(p' = p + k \cdot \text{noise}(p)\),再用扭曲后的坐标采样,相当于把图纸揉皱再展开。它不修改噪声输出、不改变层数,而是让地形在“揉皱的空间”里生长出蜿蜒曲线。

Q6. LOD(细节层次)技术的核心目标是什么?

单选题 · mid

  • A.按距离调整网格精细度← 正确答案
  • B.增加地形的噪声层数量
  • C.让每块地形精度最高
  • D.删除地形上的全部细节

答案:A。LOD 按距离决定网格精度:近处密、远处疏,距离翻倍细节降一级。它不会增加噪声层,也不会让所有地块都是最高精度——那反而会撑爆内存,更不是删除全部细节,而是动态权衡。

Q7. 归一化高度图时使用 min/max 的目的是什么?

单选题 · hard

  • A.把噪声映射到目标海拔范围← 正确答案
  • B.压缩地形文件的内存空间
  • C.消除地形中的全部河流
  • D.把噪声值全部归零处理

答案:A。归一化用 \((raw - \min) / (\max - \min)\) 把噪声值压到 0~1,再映射到设定的海拔区间 \([H_{\min}, H_{\max}]\),保证海平面、雪线的位置可控。它不压缩文件、不删河流,更不是归零,而是“换算比例尺”。

Q8. 下列哪些属于 PCG 地形生成的常用技术手段?

多选题 · mid

  • A.Perlin噪声生成基础地形← 正确答案
  • B.分形八度叠加丰富细节← 正确答案
  • C.水力侵蚀模拟真实地貌← 正确答案
  • D.手工雕刻每一座山峰

答案:ABC。A、B、C 分别是“种子、叠浪、数字雨”三个核心步骤,构成 PCG 地形生成的完整链路。D 是传统手工方式,恰恰是 PCG 想替代的,不属于程序化地形生成手段。

Q9. 分块生成地形时,哪些做法能保证块与块之间无缝衔接?

多选题 · hard

  • A.各块共享同一坐标采样函数← 正确答案
  • B.每块使用相同的随机数种子← 正确答案
  • C.各块用独立随机的噪声源
  • D.边界处做不同地形高度

答案:AB。把世界坐标直接输入同一个噪声函数(不做块内偏移),加上相同种子,两块边界处的坐标映射到相同的噪声值,高度自然连续。独立噪声源和不同的边界高度都会在接缝处产生裂缝。

Q10. 关于水力侵蚀模拟,下列说法正确的有哪些?

多选题 · extreme

  • A.沉积物容量与流速成正比← 正确答案
  • B.水流把高处泥沙搬到低处← 正确答案
  • C.侵蚀可以完全替代噪声层
  • D.循环迭代可以刻出沟壑← 正确答案

答案:ABD。A 对应容量公式 \(C_{\text{cap}} = k_c\|v\|\);B 是侵蚀的本质——高处侵蚀、低处沉积;D 说明数千颗粒子反复迭代才能累积出沟壑。C 错误:侵蚀只能雕刻已有地形,不能凭空提供山体骨架,必须先有噪声层作为基础。