🎯 面向纯初学者,适配 Cocos Creator + TypeScript。
学习路径:感性认识 → 必备数学 → 原理拆解 → 手算例子 → 手写完整代码 → FBM分形 → 实战生成地形。
前置:会基础TS语法(函数、数组、循环);数学只需要初中水平。
周期建议:7天,每天1~1.5小时。
历史小知识:Perlin噪声由 Ken Perlin 在1983年发明,用于电影《电子世界争霸战》,后来拿到奥斯卡技术成就奖。它属于相干噪声(Coherent Noise)。
生成平滑连续的随机数:位置挨在一起,数值就接近;距离越远,变化越大。
⚠️ 关键点:Perlin噪声输出不是0~1!原版2D输出大约 ‑0.707 ~ +0.707,不是严格‑1到1,所以我们需要手动归一映射到[0,1]方便做地图高度判断。
本节目标:掌握3个Perlin噪声赖以生存的工具:向量点积、线性插值Lerp、Fade五次平滑函数。这一节所有内容,后面写噪声代码会原封不动用上,不要跳过。
二维向量:\(\vec{v} = (x,y)\),代表一个方向+长度。Perlin噪声的网格顶点,存的就是这种向量,我们叫梯度向量。
向量点积公式: \[ \vec{a}\cdot\vec{b}=a_x \cdot b_x + a_y \cdot b_y \]
⚠️ 点积结果是一个数字(标量),不是向量!
通俗理解:点积可以衡量两个向量方向的相似度:
如果梯度向量朝向采样点,点积结果为正数;背离采样点,点积为负数。
几何含义:\(\vec{a}\) 在 \(\vec{b}\) 方向上的投影长度,再乘以 \(\vec{b}\) 的长度。
如果两个向量都是单位向量(长度=1):
✅ 手算示例:
\(\vec{a}=(2,3),\quad \vec{b}=(4,5)\)
\(a\cdot b = 2\times4 + 3\times5 = 8+15=23\)
噪声内例子:梯度向量 \(\vec{g}=(1,1)\),偏移向量 \(\vec{d}=(0.2,0.3)\)
点积 = \(1\times0.2 + 1\times0.3 = 0.5\)
随堂练习1:
\(\vec{g}=(-1,0),\quad \vec{d}=(0.7,0.2)\),求点积?
function lerp(a: number, b: number, t: number): number {
return a + t * (b - a);
}
示例:lerp(2, 10, 0.5) = 2 + 0.5*(8) = 6
问题:直接lerp是直线插值。如果我们直接用原始的fx、fy当作t插值,在网格边界处导数会突变,画面会出现生硬的格子接缝,这就是为什么我们需要fade。
随堂练习2:lerp(5, 20, 0.3) = ?
定义域 t ∈ [0,1]
性质:
通俗理解:把直线t,磨成S型平滑曲线。让插值在格子边界变化速度慢慢降到0,消除方块网格接缝。
function fade(t: number): number {
return t * t * t * (t * (t * 6 - 15) + 10);
}
示例:fade(0)=0,fade(1)=1,fade(0.5)=0.5
随堂练习3(理解题):为什么Perlin不用原始fx直接lerp,而要先过fade?
输入:任意浮点数坐标 \((x,y)\),例如 \((2.3, 1.6)\)
第一步:取出这个点落在网格格子的左下角整数坐标 \[ ix = \lfloor x \rfloor,\quad iy = \lfloor y \rfloor \] \(\lfloor \rfloor\) = 向下取整(Math.floor)
例子:x=2.3,ix=2;y=1.6,iy=1
这个格子四个顶点:
采样点P相对四个顶点的偏移:
fx = x - ix = 0.3
fy = y - iy = 0.6
原版Perlin的梯度表(8个固定方向向量,不使用完全随机向量,避免方向性偏差)
[ [1,1], [-1,1], [1,-1], [-1,-1],
[1,0], [-1,0], [0,1], [0,-1] ]
输入 P(x,y)
x1 = lerp(d00, d10, u)
x2 = lerp(d01, d11, u)
result = lerp(x1, x2, v)
// 伪代码,仅逻辑演示,不可直接运行
function perlin(x, y):
ix = floor(x)
iy = floor(y)
fx = x - ix
fy = y - iy
// 取四个角梯度向量
g00 = get_gradient(ix, iy)
g10 = get_gradient(ix+1, iy)
g01 = get_gradient(ix, iy+1)
g11 = get_gradient(ix+1, iy+1)
// 偏移向量点积梯度
d00 = dot(g00, (fx , fy ))
d10 = dot(g10, (fx-1, fy ))
d01 = dot(g01, (fx , fy-1))
d11 = dot(g11, (fx-1, fy-1))
u = fade(fx)
v = fade(fy)
x1 = lerp(d00, d10, u)
x2 = lerp(d01, d11, u)
return lerp(x1, x2, v)
我们取采样点 \(P(2.3, 1.6)\),假设四个顶点梯度: \(g_{00}=(1,1),\;g_{10}=(-1,0),\;g_{01}=(0,1),\;g_{11}=(-1,-1)\)
得到该点噪声值约 0.73。注意:这个是手工假设梯度的演示;真实perm查表不会出现这么大数值,仅用来演算流程。
目标:从零敲,不复制别人封装好的库,看懂每一行。下面是可直接在Cocos Creator TS运行的原版实现,带注释。
export class Perlin2D {
private perm: number[];
private grad: [number, number][] = [
[1,1],[-1,1],[1,-1],[-1,-1],
[1,0],[-1,0],[0,1],[0,-1]
];
constructor(seed:number = 0) {
// 生成0~255置换数组
this.perm = Array.from({length:256}, (_,i)=>i);
// 简单随机打乱
const rng = (n:number)=>{
const x = Math.sin(seed + n) * 10000;
return x - Math.floor(x);
}
for(let i=255; i>0; i--){
const j = Math.floor(rng(i)*(i+1));
[this.perm[i], this.perm[j]] = [this.perm[j], this.perm[i]];
}
// 复制一份到后面,变成512,防止取模越界
this.perm.push(...this.perm);
}
private fade(t:number):number {
return t*t*t*(t*(t*6 - 15)+10);
}
private lerp(a:number,b:number,t:number):number {
return a + t*(b-a);
}
// 根据ix,iy拿到梯度向量
private getGrad(ix:number, iy:number):[number,number]{
const idx = this.perm[ this.perm[ix] + iy ] % 8;
return this.grad[idx];
}
// 核心噪声函数,输入浮点数x,y,输出大约 [-0.707,0.707]
noise(x:number, y:number):number {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const [gx00, gy00] = this.getGrad(ix, iy);
const [gx10, gy10] = this.getGrad(ix+1, iy);
const [gx01, gy01] = this.getGrad(ix, iy+1);
const [gx11, gy11] = this.getGrad(ix+1, iy+1);
// 偏移向量点积梯度
const d00 = fx * gx00 + fy * gy00;
const d10 = (fx-1)*gx10 + fy * gy10;
const d01 = fx * gx01 + (fy-1)*gy01;
const d11 = (fx-1)*gx11 + (fy-1)*gy11;
const u = this.fade(fx);
const v = this.fade(fy);
const x1 = this.lerp(d00, d10, u);
const x2 = this.lerp(d01, d11, u);
return this.lerp(x1, x2, v);
}
// 辅助:归一到 [0,1],地图最常用
noise01(x:number,y:number):number{
const n = this.noise(x,y);
return (n + 0.707) / (0.707*2);
}
}
测试代码:
const p = new Perlin2D(12345);
console.log(p.noise01(1.2,3.4));
// 同一个坐标输出固定值,这就是噪声!
阶段2任务:
原生单层Perlin太柔和,像光滑塑料,游戏地形几乎全是FBM。
FBM = 多层Perlin叠加:
freq *= lacunarity; amp *= gain
加到Perlin2D类里面:
fbm(x:number,y:number, octaves:number=4, lacunarity=2, gain=0.5):number{
let total = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0; // 用来归一化总和
for(let i=0;i
调参体验:
阶段3验收:会调octave/lacunarity/gain,理解“低频做大轮廓,高频加小细节”。
逻辑:
// mapW, mapH 地图宽高,scale噪声缩放
function generateMap(perlin:Perlin2D, mapW:number, mapH:number, scale:number=0.08){
const tileMap:number[][] = [];
for(let y=0;y
拿到tileMap二维数组后,循环实例化瓦片精灵渲染地图。
function generateIsland(perlin:Perlin2D, mapW:number, mapH:number, scale=0.08){
const tileMap:number[][] = [];
const cx = mapW*0.5;
const cy = mapH*0.5;
const maxRadius = Math.min(mapW,mapH)*0.45;
for(let y=0;y
取绝对值 Math.abs(noise()),山谷会变成峡谷沟壑;适合岩浆、崎岖山脉。
turbulence(x,y,oct=4){
let sum=0,amp=1,freq=1;
for(let i=0;i
用噪声去偏移噪声采样坐标,做出扭曲蜿蜒河流、扭曲岛屿,割草游戏常用。
// 伪代码示意
ox = noise(x,y) * warpStrength;
oy = noise(x+100,y+100) * warpStrength;
h = fbm(x+ox, y+oy);
原版Perlin在高维会有网格伪影、对角方向不均匀。Simplex(单纯形噪声)是Perlin后来改进版本,改用三角形网格,各向同性更好,3D/4D计算更快。
但是:2D场景差别不大。
✅学习建议:
学习阶段手写原版Perlin噪声理解底层原理;做正式游戏项目直接使用 FastNoiseLite,经过高度优化,bug少、速度快,不用自己维护噪声实现。