2025-11-30 · game_math
【游戏中的数学】游戏中的数学 (15) - 导航与行为
AI 如何寻路?详解 A* 算法原理(启发函数、二叉堆优化、JPS)与 Boids 群体模拟(分离、对齐、内聚、空间哈希加速、力优先级截断)。
“随机”是游戏重玩价值的源泉。但计算机无法产生真正的随机,只能产生伪随机 (Pseudo-Random)。 在程序化生成 (Procedural Generation) 中,我们不仅需要随机,还需要平滑的随机,这就是噪声 (Noise) 的舞台。
本篇将依次介绍白噪声、三种主流噪声算法(Perlin / Simplex / Worley)、分形叠加原理,以及实际工程中的常见技巧。
最常见的 Random.Range(0, 1)
产生的是白噪声。
它的特点是:当前值与上一个值完全无关——频谱上各频率能量均匀分布,因此得名”白”噪声。
如果你用它来生成地形,你会得到一堆尖锐的锯齿,像地震仪的记录,而不是连绵的山脉。
// 白噪声:相邻采样点之间没有相关性
for (int x = 0; x < width; x++)
heightmap[x] = Random.Range(0f, 1f); // 每个点独立掷骰子白噪声适合离散事件——掉落率、暴击判定、洗牌,但不适合任何需要空间连续性的场景。
Ken Perlin 在 1983 年为电影《电子世界争霸战》(TRON) 发明了这种算法,并因此获得奥斯卡技术成就奖。 它的核心性质是:相近的输入产生相近的输出,即空间上连续可导。 这被称为梯度噪声 (Gradient Noise)——它在整数网格点上放置随机梯度向量,再通过插值让结果平滑过渡。
Perlin 噪声的计算分四步:
第一步:确定网格单元。 对输入坐标
(x, y)
取整,找到它所在的单位正方形的四个顶点。
第二步:生成梯度向量。 每个整数网格点通过哈希函数映射到一个伪随机的单位梯度向量 (Gradient Vector)。经典实现使用一张长度 256 的排列表 (Permutation Table) 来完成这一映射。
第三步:计算距离向量并求点积。
从四个顶点分别到 (x, y)
做距离向量,再与对应顶点的梯度向量做点积
(Dot Product),得到四个标量影响值。
第四步:插值混合。 用缓动函数 (Ease Curve) 对四个点积值进行双线性插值。Ken Perlin 最初使用 \(3t^2 - 2t^3\),后改进为更平滑的 \(6t^5 - 15t^4 + 10t^3\)(消除二阶导数不连续的问题)。
// Perlin 噪声核心伪代码 (2D)
float PerlinNoise2D(float x, float y) {
// 1) 网格坐标
int x0 = FloorToInt(x), y0 = FloorToInt(y);
int x1 = x0 + 1, y1 = y0 + 1;
// 小数部分(单元内的相对位置)
float dx = x - x0, dy = y - y0;
// 2) 四个顶点的梯度向量(由排列表 + 哈希决定)
Vector2 g00 = Gradient(Hash(x0, y0));
Vector2 g10 = Gradient(Hash(x1, y0));
Vector2 g01 = Gradient(Hash(x0, y1));
Vector2 g11 = Gradient(Hash(x1, y1));
// 3) 距离向量 · 梯度向量
float d00 = Dot(g00, new Vector2(dx, dy));
float d10 = Dot(g10, new Vector2(dx - 1, dy));
float d01 = Dot(g01, new Vector2(dx, dy - 1));
float d11 = Dot(g11, new Vector2(dx - 1, dy - 1));
// 4) 缓动插值 fade(t) = 6t^5 - 15t^4 + 10t^3
float u = Fade(dx), v = Fade(dy);
float lerp0 = Lerp(d00, d10, u);
float lerp1 = Lerp(d01, d11, u);
return Lerp(lerp0, lerp1, v); // 结果范围约 [-1, 1]
}为什么是梯度而不是直接存值? 如果直接在网格点存随机高度再插值(即 Value Noise),结果会有明显的”方块感”。梯度噪声让值在网格点上恰好为 0,变化由梯度方向决定,产生更自然的纹理。
2001 年,Ken Perlin 自己提出了对经典 Perlin 噪声的改进——Simplex Noise。
经典 Perlin 噪声存在两个问题: 1. 方向性伪影 (Directional Artifacts): 正方形网格导致沿 45° 方向出现可感知的条纹。 2. 高维性能差: 在 N 维空间中,正方形网格的一个单元有 \(2^N\) 个顶点,3D 时需要对 8 个顶点插值,4D 时 16 个。
Simplex Noise 将正方形/立方体网格替换为单纯形网格 (Simplex Grid): - 2D 中单纯形是等边三角形,而非正方形。 - 3D 中是四面体,而非立方体。 - N 维中的单纯形只有 \(N + 1\) 个顶点(而非 \(2^N\)),因此高维时计算量大幅减少。
插值方式也不同:Simplex Noise 不使用缓动曲线 + 线性插值,而是让每个顶点的贡献通过径向衰减核 \(\max(0,\; r^2 - d^2)^4\) 独立累加,其中 \(d\) 是到顶点的距离。这种方式天然连续可导,且没有方向性偏好。
| Perlin (经典) | Simplex | |
|---|---|---|
| 网格形状 | 正方形 / 立方体 | 三角形 / 四面体 |
| N 维顶点数 | \(2^N\) | \(N+1\) |
| 伪影 | 有方向性条纹 | 极少 |
| 计算复杂度 | \(O(2^N)\) | \(O(N^2)\) |
注意: Simplex Noise 的原始论文实现曾受专利保护(美国专利 6867776,2022 年已过期)。实践中可使用 OpenSimplex 或 OpenSimplex2 等无专利替代实现。
1996 年 Steven Worley 提出的细胞噪声 (Cellular Noise) 走了一条完全不同的路。
结果看起来像 Voronoi 图:每个特征点周围形成一个”细胞”,细胞边界处值最大,中心处值为 0。
朴素实现需要遍历所有特征点 (\(O(N)\))。实际中将空间划分为等大网格,每个格子只放一个特征点,采样时只需检查周围 \(3^D\) 个格子(2D 检查 9 个,3D 检查 27 个),变成常数时间。
| 特性 | Perlin | Simplex | Worley |
|---|---|---|---|
| 视觉风格 | 平滑连绵 | 平滑连绵(更均匀) | 细胞状 / Voronoi |
| 高维性能 | 较差 (\(2^N\)) | 优秀 (\(N+1\)) | 中等(邻域搜索) |
| 方向伪影 | 有 | 极少 | 无 |
| 典型用途 | 地形、云、雾 | 地形、云(替代 Perlin) | 石纹、鳞片、焦散 |
| 实现难度 | 中等 | 较高(坐标变换) | 简单 |
在现代项目中,Simplex 通常是 Perlin 的直接上位替代;而 Worley 噪声提供了完全不同的美学,常与前两者组合使用。
单一的噪声层看起来太”圆润”了。真实的山脉既有公里级别的大轮廓,也有米级别的小石块。 为了模拟这种多尺度细节,我们将多层不同频率的噪声叠加——这称为分形布朗运动 (Fractional Brownian Motion, fBm),也叫倍频程叠加 (Octave Stacking)。
\[ \text{fBm}(x) = \sum_{i=0}^{N-1} A_i \cdot \text{Noise}(x \cdot F_i) \]
三个关键参数: - Frequency (频率):
采样点的密度。每层频率通常是上一层的 2 倍。 -
Amplitude (振幅):
该层的高度范围。每层振幅是上一层的 persistence
倍。 - Lacunarity (间隙度):
频率的层间倍率。默认为 2.0,增大它会让细节更密集。
/// <summary>
/// 分形噪声:多倍频程叠加
/// </summary>
float FractalNoise(float x, float y, int octaves, float persistence, float lacunarity) {
float total = 0f;
float frequency = 1f;
float amplitude = 1f;
float maxValue = 0f; // 用于归一化
for (int i = 0; i < octaves; i++) {
total += PerlinNoise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence; // 典型值 0.5:每层振幅减半
frequency *= lacunarity; // 典型值 2.0:每层频率翻倍
}
return total / maxValue; // 归一化到 [-1, 1]
}参数调节直觉: -
octaves = 1: 只有大山轮廓,没有细节。 -
octaves = 6~8: 大轮廓 +
丰富的岩石细节,适合地形。 - persistence = 0.3:
高频细节很弱,地形偏圆润。 - persistence = 0.7:
高频细节强烈,地形粗粝嶙峋。 -
lacunarity > 2.0:
细节频率跳得更快,纹理更”碎”。
程序化生成的一大优势是确定性 (Determinism)——同一个种子 (Seed) 永远产生同一个世界。
实现要点: - 用种子初始化排列表 (Permutation
Table),而不是使用固定的默认表。 -
同一帧内不要使用全局
Random,否则调用顺序变化会导致结果不可复现。推荐为每个系统创建独立的随机实例。
- 存档只需保存种子值 + 玩家修改部分,而非整张地图。
// 为地形和植被分别创建独立随机源,互不干扰
System.Random terrainRng = new System.Random(worldSeed);
System.Random vegetationRng = new System.Random(worldSeed ^ 0x5DEECE66D);普通噪声在边界处不连续。要制作可重复平铺的纹理,有两种常用方法:
(u, v) 映射到 4D 环面上的圆环坐标,再采样 4D
噪声。相当于在高维空间里走了一个”甜甜圈”。将噪声的输出反馈为另一次噪声采样的输入偏移,可以产生极其有机的扭曲效果——类似大理石纹、烟雾或奇幻地图。
// 域扭曲:用噪声扭曲采样坐标
float DomainWarp(float x, float y, float warpStrength) {
float offsetX = FractalNoise(x, y, 4, 0.5f, 2f);
float offsetY = FractalNoise(x + 5.2f, y + 1.3f, 4, 0.5f, 2f);
return FractalNoise(x + offsetX * warpStrength,
y + offsetY * warpStrength,
6, 0.5f, 2f);
}多次迭代域扭曲(即用扭曲结果再扭曲)可以产生更加复杂的图案,Inigo Quilez 的文章中有许多精彩的示例。
scale(采样坐标的缩放因子),它决定了噪声的”基础波长”。地形通常
scale = 0.005 ~ 0.02。octaves: 从 1
开始逐层增加,直到细节足够。注意每增加一层都有性能开销。persistence:
控制地形的粗粝程度。< 上一篇: 骨骼动画 | 回到目录 | 下一篇: 导航与行为 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
AI 如何寻路?详解 A* 算法原理(启发函数、二叉堆优化、JPS)与 Boids 群体模拟(分离、对齐、内聚、空间哈希加速、力优先级截断)。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。
“随机”是游戏重玩价值的源泉。但计算机无法产生真正的随机,只能产生伪随机 (Pseudo-Random)。 在程序化生成 (Procedural Generation) 中,我们不仅需要随机,还需要平滑的随机,这就是噪声 (Noise) 的舞台。
本篇将依次介绍白噪声、三种主流噪声算法(Perlin / Simplex / Worley)、分形叠加原理,以及实际工程中的常见技巧。
最常见的 Random.Range(0, 1)
产生的是白噪声。
它的特点是:当前值与上一个值完全无关——频谱上各频率能量均匀分布,因此得名”白”噪声。
如果你用它来生成地形,你会得到一堆尖锐的锯齿,像地震仪的记录,而不是连绵的山脉。
// 白噪声:相邻采样点之间没有相关性
for (int x = 0; x < width; x++)
heightmap[x] = Random.Range(0f, 1f); // 每个点独立掷骰子白噪声适合离散事件——掉落率、暴击判定、洗牌,但不适合任何需要空间连续性的场景。
Ken Perlin 在 1983 年为电影《电子世界争霸战》(TRON) 发明了这种算法,并因此获得奥斯卡技术成就奖。 它的核心性质是:相近的输入产生相近的输出,即空间上连续可导。 这被称为梯度噪声 (Gradient Noise)——它在整数网格点上放置随机梯度向量,再通过插值让结果平滑过渡。
Perlin 噪声的计算分四步:
第一步:确定网格单元。 对输入坐标
(x, y)
取整,找到它所在的单位正方形的四个顶点。
第二步:生成梯度向量。 每个整数网格点通过哈希函数映射到一个伪随机的单位梯度向量 (Gradient Vector)。经典实现使用一张长度 256 的排列表 (Permutation Table) 来完成这一映射。
第三步:计算距离向量并求点积。
从四个顶点分别到 (x, y)
做距离向量,再与对应顶点的梯度向量做点积
(Dot Product),得到四个标量影响值。
第四步:插值混合。 用缓动函数 (Ease Curve) 对四个点积值进行双线性插值。Ken Perlin 最初使用 \(3t^2 - 2t^3\),后改进为更平滑的 \(6t^5 - 15t^4 + 10t^3\)(消除二阶导数不连续的问题)。
// Perlin 噪声核心伪代码 (2D)
float PerlinNoise2D(float x, float y) {
// 1) 网格坐标
int x0 = FloorToInt(x), y0 = FloorToInt(y);
int x1 = x0 + 1, y1 = y0 + 1;
// 小数部分(单元内的相对位置)
float dx = x - x0, dy = y - y0;
// 2) 四个顶点的梯度向量(由排列表 + 哈希决定)
Vector2 g00 = Gradient(Hash(x0, y0));
Vector2 g10 = Gradient(Hash(x1, y0));
Vector2 g01 = Gradient(Hash(x0, y1));
Vector2 g11 = Gradient(Hash(x1, y1));
// 3) 距离向量 · 梯度向量
float d00 = Dot(g00, new Vector2(dx, dy));
float d10 = Dot(g10, new Vector2(dx - 1, dy));
float d01 = Dot(g01, new Vector2(dx, dy - 1));
float d11 = Dot(g11, new Vector2(dx - 1, dy - 1));
// 4) 缓动插值 fade(t) = 6t^5 - 15t^4 + 10t^3
float u = Fade(dx), v = Fade(dy);
float lerp0 = Lerp(d00, d10, u);
float lerp1 = Lerp(d01, d11, u);
return Lerp(lerp0, lerp1, v); // 结果范围约 [-1, 1]
}为什么是梯度而不是直接存值? 如果直接在网格点存随机高度再插值(即 Value Noise),结果会有明显的”方块感”。梯度噪声让值在网格点上恰好为 0,变化由梯度方向决定,产生更自然的纹理。
2001 年,Ken Perlin 自己提出了对经典 Perlin 噪声的改进——Simplex Noise。
经典 Perlin 噪声存在两个问题: 1. 方向性伪影 (Directional Artifacts): 正方形网格导致沿 45° 方向出现可感知的条纹。 2. 高维性能差: 在 N 维空间中,正方形网格的一个单元有 \(2^N\) 个顶点,3D 时需要对 8 个顶点插值,4D 时 16 个。
Simplex Noise 将正方形/立方体网格替换为单纯形网格 (Simplex Grid): - 2D 中单纯形是等边三角形,而非正方形。 - 3D 中是四面体,而非立方体。 - N 维中的单纯形只有 \(N + 1\) 个顶点(而非 \(2^N\)),因此高维时计算量大幅减少。
插值方式也不同:Simplex Noise 不使用缓动曲线 + 线性插值,而是让每个顶点的贡献通过径向衰减核 \(\max(0,\; r^2 - d^2)^4\) 独立累加,其中 \(d\) 是到顶点的距离。这种方式天然连续可导,且没有方向性偏好。
| Perlin (经典) | Simplex | |
|---|---|---|
| 网格形状 | 正方形 / 立方体 | 三角形 / 四面体 |
| N 维顶点数 | \(2^N\) | \(N+1\) |
| 伪影 | 有方向性条纹 | 极少 |
| 计算复杂度 | \(O(2^N)\) | \(O(N^2)\) |
注意: Simplex Noise 的原始论文实现曾受专利保护(美国专利 6867776,2022 年已过期)。实践中可使用 OpenSimplex 或 OpenSimplex2 等无专利替代实现。
1996 年 Steven Worley 提出的细胞噪声 (Cellular Noise) 走了一条完全不同的路。
结果看起来像 Voronoi 图:每个特征点周围形成一个”细胞”,细胞边界处值最大,中心处值为 0。
朴素实现需要遍历所有特征点 (\(O(N)\))。实际中将空间划分为等大网格,每个格子只放一个特征点,采样时只需检查周围 \(3^D\) 个格子(2D 检查 9 个,3D 检查 27 个),变成常数时间。
| 特性 | Perlin | Simplex | Worley |
|---|---|---|---|
| 视觉风格 | 平滑连绵 | 平滑连绵(更均匀) | 细胞状 / Voronoi |
| 高维性能 | 较差 (\(2^N\)) | 优秀 (\(N+1\)) | 中等(邻域搜索) |
| 方向伪影 | 有 | 极少 | 无 |
| 典型用途 | 地形、云、雾 | 地形、云(替代 Perlin) | 石纹、鳞片、焦散 |
| 实现难度 | 中等 | 较高(坐标变换) | 简单 |
在现代项目中,Simplex 通常是 Perlin 的直接上位替代;而 Worley 噪声提供了完全不同的美学,常与前两者组合使用。
单一的噪声层看起来太”圆润”了。真实的山脉既有公里级别的大轮廓,也有米级别的小石块。 为了模拟这种多尺度细节,我们将多层不同频率的噪声叠加——这称为分形布朗运动 (Fractional Brownian Motion, fBm),也叫倍频程叠加 (Octave Stacking)。
\[ \text{fBm}(x) = \sum_{i=0}^{N-1} A_i \cdot \text{Noise}(x \cdot F_i) \]
三个关键参数: - Frequency (频率):
采样点的密度。每层频率通常是上一层的 2 倍。 -
Amplitude (振幅):
该层的高度范围。每层振幅是上一层的 persistence
倍。 - Lacunarity (间隙度):
频率的层间倍率。默认为 2.0,增大它会让细节更密集。
/// <summary>
/// 分形噪声:多倍频程叠加
/// </summary>
float FractalNoise(float x, float y, int octaves, float persistence, float lacunarity) {
float total = 0f;
float frequency = 1f;
float amplitude = 1f;
float maxValue = 0f; // 用于归一化
for (int i = 0; i < octaves; i++) {
total += PerlinNoise2D(x * frequency, y * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence; // 典型值 0.5:每层振幅减半
frequency *= lacunarity; // 典型值 2.0:每层频率翻倍
}
return total / maxValue; // 归一化到 [-1, 1]
}参数调节直觉: -
octaves = 1: 只有大山轮廓,没有细节。 -
octaves = 6~8: 大轮廓 +
丰富的岩石细节,适合地形。 - persistence = 0.3:
高频细节很弱,地形偏圆润。 - persistence = 0.7:
高频细节强烈,地形粗粝嶙峋。 -
lacunarity > 2.0:
细节频率跳得更快,纹理更”碎”。
程序化生成的一大优势是确定性 (Determinism)——同一个种子 (Seed) 永远产生同一个世界。
实现要点: - 用种子初始化排列表 (Permutation
Table),而不是使用固定的默认表。 -
同一帧内不要使用全局
Random,否则调用顺序变化会导致结果不可复现。推荐为每个系统创建独立的随机实例。
- 存档只需保存种子值 + 玩家修改部分,而非整张地图。
// 为地形和植被分别创建独立随机源,互不干扰
System.Random terrainRng = new System.Random(worldSeed);
System.Random vegetationRng = new System.Random(worldSeed ^ 0x5DEECE66D);普通噪声在边界处不连续。要制作可重复平铺的纹理,有两种常用方法:
(u, v) 映射到 4D 环面上的圆环坐标,再采样 4D
噪声。相当于在高维空间里走了一个”甜甜圈”。将噪声的输出反馈为另一次噪声采样的输入偏移,可以产生极其有机的扭曲效果——类似大理石纹、烟雾或奇幻地图。
// 域扭曲:用噪声扭曲采样坐标
float DomainWarp(float x, float y, float warpStrength) {
float offsetX = FractalNoise(x, y, 4, 0.5f, 2f);
float offsetY = FractalNoise(x + 5.2f, y + 1.3f, 4, 0.5f, 2f);
return FractalNoise(x + offsetX * warpStrength,
y + offsetY * warpStrength,
6, 0.5f, 2f);
}多次迭代域扭曲(即用扭曲结果再扭曲)可以产生更加复杂的图案,Inigo Quilez 的文章中有许多精彩的示例。
scale(采样坐标的缩放因子),它决定了噪声的”基础波长”。地形通常
scale = 0.005 ~ 0.02。octaves: 从 1
开始逐层增加,直到细节足够。注意每增加一层都有性能开销。persistence:
控制地形的粗粝程度。< 上一篇: 骨骼动画 | 回到目录 | 下一篇: 导航与行为 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
AI 如何寻路?详解 A* 算法原理(启发函数、二叉堆优化、JPS)与 Boids 群体模拟(分离、对齐、内聚、空间哈希加速、力优先级截断)。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。