艺术家的作品(图片来自 https://northloop.org/event/black-history-month/)
Perlin 噪声的发明者 Ken Perlin 在 1980年的时候被安排给电影 Tron 生成更真实的纹理,最终他通过一些噪声实现了那些纹理,并因此获得了奥斯卡奖。那到底什么是噪声呢?让我们先从随机函数开始了解噪声。
“随机函数” 中的 “随机” 是指:重复多次调用该函数,调用后返回值之间是没有关联性的。为了更加形象,我将这些返回值映射为蓝色小球的 Y 轴坐标,可以看出相邻小球之间变化较大,整个曲线凌乱无序。随机函数又分为 “非确定性随机” 和 “确定性随机” 。
多次调用随机函数的返回值
...
for(let i = 0; i <= cols - 1; i++) {
// 随机值
y1 = random()*100 + 50;
ellipse(i*step, y1, 10);
}
...
“非确定性随机” 是指:不仅重复调用该函数的返回值之间没有关联性,且每次运行程序,得到的结果也不同。反复手动刷新页面,会得到不一样的结果曲线。
反复刷新页面得到不同结果
还有一种随机函数,其返回值是随机的,但是每次刷新页面,会得到相同的结果曲线,这种则被称为确定性随机,也称伪随机。
如何得到一个确定性随机函数呢? 这里演示一种很简单的方法:使用sin函数,通过提高sin函数的频率,得到一个伪随机函数。
...
float pseudorandom (float x) {
return sin(x*10000.);
}
...
不断提升sin函数的频率得到的结果
我们先将一个噪声的值映射到蓝色小球的 Y 轴坐标,感受一下噪声,如下图:
简单的一维噪声
...
for (let i = 0; i <= cols - 1; i++) {
y1 = noise(i * step / 100) * 100 + 50;
ellipse(i * step, y1, 10);
}
...
与随机函数不同,可以看到相邻小球的之间的变化是 比较小
的,整个图形看上去是一条连续
的曲线。而且无论刷新页面多少次,曲线都是不变
的,但是单看曲线上的每个小球,又是有一定的随机性
。
利用噪声 “随机” 和 “连续” 这两个特质,我们可以得到很多效果,而且噪声可以从一维扩展到多维。
由一个变量控制:noise( parameter ),我们可以传入小球的 x 坐标,然后将函数的返回值映射为小球的 y 坐标,就能得到一个随机的、自然的小球运动动画。
小球的 Y 轴坐标由一维噪声生成
接收两个参数:noise ( parameterA, parameterB ),我们可以传入 x、y 坐标:
...
let angle = map(noise(this.pos.x * velFactor, this.pos.y * velFactor),0, 1, 0, 720);
this.vel = createVector(cos(angle),sin(angle));
this.pos.add(this.vel);
...
二维噪声生成的粒子
...
this.end = createVector(
(outterRadius + 150 * noise((this.start.x + frameCount) * velFactor, (this.start.y + frameCount) * velFactor)) * cos(this.angle),
(outterRadius + 150 * noise((this.start.x + frameCount) * velFactor, (this.start.y + frameCount) * velFactor)) * sin(this.angle)
);
let r = map(noise((this.end.x + frameCount) * velFactor * 0.1), 0, 1, 60, 255);
let g = map(noise((this.end.y + frameCount) * velFactor * 0.1), 0, 1, 90, 255);
let b = map(noise((this.end.x + frameCount) * velFactor * 0.1, (this.end.y + frameCount) * velFactor * 0.1), 0, 1, 80, 255);
stroke(r, g, b, 255);
fill(r, g, b, 255);
line(this.start.x, this.start.y, this.end.x, this.end.y);
ellipse(this.end.x + 50, this.end.y + 50, 5, 5);
ellipse(this.end.x - 50, this.end.y - 50, 5, 5);
ellipse(this.end.x * 0.2, this.end.y * 0.2, 5, 5);
...
二维噪声映射得到的粒子
接收n个参数:noise(parameterA, parameterB, ..., parameterN),下面 “太阳” 的例子中传入了 4 个变量来控制噪声,其中一个变量还是实时变化的,从而形成了动态的燃烧效果。
...
float fbm (vec4 p) {
// 对噪声的迭代次数
int octaves = 6;
// 初始幅度
float amplitude = 1.;
// 初始频率
float frequency = 1.;
// 幅度每次的衰减倍数
float attenuation = 0.9;
float mixValue = 0.;
for(int i = 0; i < octaves; i++) {
mixValue += snoise4(p*frequency)*amplitude;
p.w += 100.;
amplitude *= attenuation;
frequency *= 2.;
}
return mixValue;
}
void main() {
// time由cpu传递给gpu,它一直在增加
vec4 p1 = vec4(vNormal*3., time*0.05);
float noiseParamer1 = fbm(p1);
vec4 p2 = vec4(vNormal*2., time*0.05);
float noiseParamer2 = snoise4(p2);
gl_FragColor = vec4(noiseParamer1*noiseParamer2);
gl_FragColor = vec4(1.,0.5,0.2,1.) - gl_FragColor + vec4(0.2) ;
}
...
将噪声映射到物体的材质
...
randomSeed(99);
class Noise {
constructor(){
this.points = [];
step = width / cols;
for (let i = 0; i <= cols; i++ ) {
let x = i*step;
let y = random()*200;
this.points.push({x, y});
}
}
...
class Noise {
...
linear(x) {
let index = floor(x);
let fraction = fract(x);
let prevVector = createVector(this.points[index].x, this.points[index].y);
let nextVector = createVector(this.points[index + 1].x, this.points[index + 1].y);
return p5.Vector.lerp(prevVector, nextVector, fraction);
}
...
}
线性插值
...
curve(x) {
let index = floor(x);
let fraction = fract(x);
let index1 = index > 0 ? index - 1 : 0;
let index2 = index + 1;
let index3 = index2 === this.points.length ? index2 : index + 2;
let p1 = {
x: this.points[index1].x,
y: this.points[index1].y
}
let p2 = {
x: this.points[index].x,
y: this.points[index].y
}
let p3 = {
x: this.points[index2].x,
y: this.points[index2].y
}
let p4 = {
x: this.points[index3].x,
y: this.points[index3].y
}
return createVector(
curvePoint(p1.x, p2.x, p3.x, p4.x, fraction),
curvePoint(p1.y, p2.y, p3.y, p4.y, fraction)
)
}
...
平滑插值
在一维空间里,我们是在相邻的两个点之间进行插值,来到二维空间,则需要在点周围的四个点之间进行插值。
...
void main() {
vUv = uv*10.;
// 获得整数部分
vec2 i = floor(vUv);
// 获得小数部分
vec2 f = fract(vUv);
vColor = vec4(f,1.,1.);
vec4 mvPosition = modelViewMatrix*vec4(position,1.);
gl_Position = projectionMatrix*mvPosition;
}
...
栅格化平面得到的图像
...
// 自定义随机函数
float rand(vec2 p){
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
float noise(vec2 p){
// 获得整数部分
vec2 i = floor(p);
// 获得小数部分
vec2 f = fract(p);
// 左下角
vec2 lb = vec2(i);
// 右下角
vec2 rb = vec2(i.x+1., i.y);
// 右上角
vec2 rt = vec2(i.x+1., i.y+1.);
// 左上角
vec2 lt = vec2(i.x, i.y+1.);
// 将四个顶点的值插值得到当前点的值
float mixValue =mix(
mix(rand(lb), rand(rb), smoothstep(0., 1., f.x)),
mix(rand(lt), rand(rt), smoothstep(0., 1., f.x)),
smoothstep(0., 1., f.y)
);
return mixValue;
}
void main() {
vUv = uv*10.;
vColor = vec4(vec3(noise(vUv)), 1.);
vec4 mvPosition = modelViewMatrix*vec4(position, 1.);
gl_Position = projectionMatrix*mvPosition;
}
...
值噪声
这种通过给四个点随机插值得到的噪声叫做“值噪声”,还有另外一种常见的噪声叫做“梯度噪声,它是通过给四个点随机的梯度,再进行插值得到。
梯度、距离向量示意
...
// 自定义随机函数, 返回类型为vec2
vec2 rand(vec2 st){
st = vec2(dot(st, vec2(127.1, 311.7)),
dot(st, vec2(269.5, 183.3)));
return -1.0 + 2.0*fract(sin(st)*43758.5453123);
}
float noise(vec2 p){
// 获得整数部分
vec2 i = floor(p);
// 获得小数部分
vec2 f = fract(p);
// 左下角
vec2 lb = vec2(i);
// 右下角
vec2 rb = vec2(i.x+1., i.y);
// 右上角
vec2 rt = vec2(i.x+1., i.y+1.);
// 左上角
vec2 lt = vec2(i.x, i.y+1.);
// 给4个顶点随机的梯度向量,然后将顶点的梯度向量与顶点到当前点的距离向量进行点乘,最后在这四个点乘结果中进行插值
float mixValue =mix(
mix(dot(rand(lb), f - vec2(0.0, 0.0)), dot(rand(rb), f - vec2(1.0, 0.0)), smoothstep(0., 1., f.x)),
mix(dot(rand(lt), f - vec2(0.0, 1.0)), dot(rand(rt), f - vec2(1.0, 1.0)), smoothstep(0., 1., f.x)),
smoothstep(0., 1., f.y)
);
return mixValue;
}
void main() {
vUv = uv*10.;
vColor = vec4(vec3(noise(vUv) + 0.1), 1.);
vec4 mvPosition = modelViewMatrix*vec4(position, 1.);
gl_Position = projectionMatrix*mvPosition;
}
...
梯度噪声
n维噪声这里不再详细实现,原理类似于二维噪声,只是插值的对象逐渐增加。
分形
我们还可以将多次噪声处理的结果进行叠加,从而得到更多、更自然的细节,这项技术被称为“分形”。分形处理中的每次循环称为一个 “octave”,每次迭代时都以一定的倍数提高频率,降低幅度。
...
float fbm (vec2 p) {
// 对噪声的迭代次数
int octaves = 8;
// 初始幅度
float amplitude = 1.;
// 初始频率
float frequency = 1.;
// 幅度每次的衰减倍数
float attenuation = 0.5;
// 频率每次增加的次数
float increase = 2.;
float mixValue = 0.;
for(int i = 0; i < octaves; i++) {
// 将每次噪声函数处理的结果进行累加
mixValue += amplitude*noise(p*frequency);
amplitude *= attenuation;
frequency *= increase;
}
return mixValue;
}
...
分形噪声
分形噪声通常被用在云朵、大理石等自然纹理的生成,比如下图中绵延的山脉和云海都是由分形技术实现的。
分形技术生成
...
void main() {
vUv = uv*20.;
vColor = vec4(vec3(fbm(vUv)*0.5 + 0.8), 1.);
vec3 temPosition = vec3(position.x, position.y*(fbm(vUv))*2. , position.z);
vec4 mvPosition = modelViewMatrix*vec4(temPosition, 1.);
gl_Position = projectionMatrix*mvPosition;
}
...
通过上面这些案例,你应该已经能感受到噪声艺术的迷人之处吧!而本文仅仅是掀开了噪声的一个小小的角落,噪声是如此的变化多样,将各种噪声算法进行组合迭代,加入各种插值算法,调整各个参与的变量等等都能够带给我们无穷无尽的惊喜。噪声算法的设计初衷是将大自然各种各样的天然纹理用数字图像表示出来,然而在今天,如今已经被广泛的应用在了音乐可视化、代码生成艺术、物理与仿真等各个领域,未来将成为视觉艺术家的手中利器。
关注我们👇一起成长