观察下面的动图,你是否对其流动的线条顺滑性感到惊讶?
当我第一次看到这张图的时候,第一反应就是,这不就是一张随机的运动图嘛,把每粒子的运动轨迹位置添加一个通过random函数获取数值不就可以了?
但是后来在我实际编写代码实现的过程中,通过random函数表现出粒子运动效果看起来很杂乱无序,没有这种视觉上的顺滑感。
仔细观察上图,会发现这种流线的运动看似随机,但是感觉有种规律。后经翻阅资料后,找到背后相关的技术原理:Perlin Noise.
很多小伙伴在编写粒子运动的代码的过程中,使用随机数生成器创建“随机数”来使粒子对象的运动和行为显得更自然,这种随机数往往代表不可预测性。随机数生成器肯定有这方面的作用,但有时其输出可能过于杂乱而显得不自然。
有时候会发现,在手写板上书写的笔迹显得异常的丑,计算机将其稍加润色一下,整个笔迹就显得十分的自然和美观;在玩3D游戏中,会发现一些游戏中的角色运动的十分有规律且自然;湖面波浪的起伏。
这些自然效果的表现,都可以通过 Perlin Noise 表现出来。
Perlin noise
Perlin noise是一个随机序列生成器,它的表现比标准random更自然、更和谐。它是由Ken Perlin在20世纪80年代开发的,并已用于图形应用程序,以生成程序纹理、形状、地形和其他看似有机的形式。
Perlin 噪声常见实现形式为二维、三维或四维函数,但可以定义为任意数量的维。实现Perlin Noise通常包括三个步骤:网格定义;点积;插值。
网格定义
二维网格的向量
定义一个n维网格,其中每个网格交点都有一个与其关联且固定的随机n维单位长度渐变向量;但在一维情况下,梯度是介于 -1 和 1 之间的随机标量。
点积
每个点与其最近的网格节点梯度值的点积
输入一个点(二维的话就是二维坐标,三维就是三维坐标,n维的就是n个坐标),我们找到和它相邻的那些晶格顶点(二维下有4个,三维下有8个,n维下有2^n个),计算该点到各个晶格顶点的距离向量,再分别与顶点上的梯度向量做点乘,得到个点乘结果。
插值
使用缓和的曲线来计算它们的权重和。由高等数学可以知道,函数越是高阶可导函数曲线越是平滑,在一阶导满足连续性,但它的二阶导在晶格顶点处(即t = 0或t = 1)不为0,会造成明显的不连续性。在二阶导上仍然满足连续性。
举例说明:
此处蓝点代表2D平面输入的(x,y)坐标点和其周围的4个晶体格顶点。
这里的蓝点代表输入坐标
其他4个晶体格顶点单位坐标
在4个单位坐标的每个坐标上,生成所谓的伪随机梯度向量。该梯度矢量定义了一个正方向(指向它的方向),当然也定义了一个负方向(指向它的相反方向)。 伪随机意味着,对于输入到梯度矢量方程中的任何整数集,总是会出现相同的结果。因此,这似乎是随机的,但实际上并非如此。
另外,这意味着每个积分坐标都有其“自己的”梯度,如果梯度函数不变,则该梯度将永远不变。
接下来,我们需要计算从给定点到网格上8个周围点的4个矢量。下面显示了2D的示例情况。
距离矢量示例
接下来,我们取两个向量(梯度向量和距离向量)之间的点积。这给了我们最终的影响力值:
grad.x * dist.x + grad.y * dist.y + grad.z * dist.z
之所以可行,是因为两个向量的点积等于两个向量之间的角度的余弦值乘以这些向量的大小:
dot(vec1,vec2) = cos(angle(vec1,vec2)) * vec1.length * vec2.length
换句话说,如果两个向量指向相同的方向,则点积将等于:
1 * vec1.length * vec2.length
..如果两个向量指向相反的方向,则点积将等于:
-1 * vec1.length * vec2.length
如果两个向量垂直,则点积为0 。因此,点积的结果在梯度方向上将为正,而在相反的方向上将为负。这就是梯度矢量定义正方向和负方向的方式。这是代表这些正面/负面影响的图形:
因此,现在我们要做的就是在这4个值之间进行插值,以便在4个网格点之间获得某种加权平均值。解决这个问题的方法很简单:像这样对平均值进行平均或者加权平均值。
应用
一维
Perlin函数
控制虚拟人物 | 在游戏中,使用柏林噪声不断调整虚拟人物的关节位置,使其看起来更生动。 |
绘制草图 | 电脑画的线总是笔直的,这会使它们看起来不自然和不友好。可以使用Perlin噪波为绘制线算法引入抖动,使其看起来像是用手绘制的。
|
二维
Perlin函数
地形 | Perlin Noise 用来表现地形的连绵起伏。 |
云 | Perlin Noise 也适合用于云层渲染。 |
生成材质 | Perlin Noise 生成各种纹理,比重复的平铺纹理贴图更易于查看。 |
三维
Perlin函数
3D云 | 用来产生体积云 |
云动画 | 用3D Perlin Noise函数产生2维动画 |
固体纹理 | 有些渲染/光线跟踪程序,如POVray,通过从三维纹理中直接切割对象来应用纹理。 |
案例
int noiseScale = 500, noiseStrength = 1;
int num = 1000;
Particle[] particles = new Particle[num];
void setup(){
// windows size = 800 * 600
size(800,600);
// no stroke
noStroke();
// paint 1000 paticles
for(int i = 0; i < num; ++i){
PVector loc = new PVector(random(width * 1.2), random(height * 1.2), 2);
float angle = random(TWO_PI);
PVector dir = new PVector(cos(angle),sin(angle));
float speed = random(0.5, 2);
particles[i] = new Particle(loc, dir, speed);
}
}
void draw(){
fill(0,10);
noStroke();
rect(0,0,width,height);
for(int i = 0; i < num; ++i){
particles[i].run();
}
}
class Particle{
// 3 Elements : Location Velocity Direction
PVector loc, vel, dir;
// Speed
float speed;
// direction change
int d = 1;
Particle(PVector _loc, PVector _dir, float _speed){
loc = _loc;
dir = _dir;
speed = _speed;
}
// run to next position
void run(){
// calc the next position
move();
// check position is out of edge
checkEdge();
// paint particle
update();
}
void move(){
// generate the random angle
float angle = noise(loc.x/noiseScale, loc.y/noiseScale, frameCount/noiseScale) * TWO_PI * noiseStrength;
// deltaX = x-axis increment
dir.x = cos(angle);
// deltaY = y-axis increment
dir.y = sin(angle);
// vel = [delatX, deltaY]
vel = dir.get();
// speed num
vel.mult(speed * d);
// nextlocation = curPosition + vel
loc.add(vel);
}
void checkEdge(){
if(loc.x < 0 || loc.x > width || loc.y < 0 || loc.y > height){
// factor 1.2 ensure the loc.x near edge
loc.x = random(width * 1.2);
loc.y = random(height);
}
}
void update(){
fill(255);
ellipse(loc.x, loc.y, loc.z, loc.z);
}
恒成立
上海理工大学光电硕士
努力将数字公式可视化
- END -
获取年会分享录播
请加入知识星球
置顶文件(不定期放出)
戳星球二维码 ↓