用噪声,做视觉艺术家

云朵、山脉、泥土、树木都是大自然的鬼斧神工,但如何使用计算机模拟出这些自然界的纹理呢?你可能猜不到,我们可以通过噪声来实现。噪声,是一种图像算法,主要用来模拟生成各种纹理。噪声在生成艺术中扮演着重要角色,开发者通过各种噪声的组合,帮助艺术家完成作品。

用噪声,做视觉艺术家艺术家的作(图片来自 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);
}
...

1. 非确定性随机

“非确定性随机” 是指:不仅重复调用该函数的返回值之间没有关联性,且每次运行程序,得到的结果也不同。反复手动刷新页面,会得到不一样的结果曲线。用噪声,做视觉艺术家反复刷新页面得到不同结果

2. 确定性随机

还有一种随机函数,其返回值是随机的,但是每次刷新页面,会得到相同的结果曲线,这种则被称为确定性随机,也称伪随机。

如何得到一个确定性随机函数呢? 这里演示一种很简单的方法:使用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);
}
...

与随机函数不同,可以看到相邻小球的之间的变化是 比较小的,整个图形看上去是一条连续的曲线。而且无论刷新页面多少次,曲线都是不变的,但是单看曲线上的每个小球,又是有一定的随机性 。

利用噪声 “随机” 和 “连续” 这两个特质,我们可以得到很多效果,而且噪声可以从一维扩展到多维。

1. 一维噪声

由一个变量控制:noise( parameter ),我们可以传入小球的 x 坐标,然后将函数的返回值映射为小球的 y 坐标,就能得到一个随机的、自然的小球运动动画。

用噪声,做视觉艺术家

小球的 Y 轴坐标由一维噪声生成

2. 二维噪声

接收两个参数: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);
...

用噪声,做视觉艺术家二维噪声映射得到的粒子

3. n 维噪声

接收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) ;
}
...

用噪声,做视觉艺术家将噪声映射到物体的材质

如何得到一个噪声

1. 一维噪声

  • 第一步:获取一些离散的随机值。通过 random() 函数来获取随机值,为了演示方便,我们调用 randomSeed来保证每一步得到的随机值是一样的。

...
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)
)
}
...

用噪声,做视觉艺术家平滑插值

2. 二维噪声

在一维空间里,我们是在相邻的两个点之间进行插值,来到二维空间,则需要在点周围的四个点之间进行插值。

  • 第一步:将平面进行栅格化,得到方块儿格子

...
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;
}
...

用噪声,做视觉艺术家栅格化平面得到的图像

  • 第二步:给当前点所在方格的四个顶点取随机值,然后进行插值,这里采用的不是线性插值,而是通过smootstep做曲线插值。

...
// 自定义随机函数
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;
}
...

小结

通过上面这些案例,你应该已经能感受到噪声艺术的迷人之处吧!而本文仅仅是掀开了噪声的一个小小的角落,噪声是如此的变化多样,将各种噪声算法进行组合迭代,加入各种插值算法,调整各个参与的变量等等都能够带给我们无穷无尽的惊喜。噪声算法的设计初衷是将大自然各种各样的天然纹理用数字图像表示出来,然而在今天,如今已经被广泛的应用在了音乐可视化、代码生成艺术、物理与仿真等各个领域,未来将成为视觉艺术家的手中利器。



用噪声,做视觉艺术家

 关注我们?一起成长 



本篇文章来源于微信公众号:腾讯云设计中心

UI/UX

B端表单设计常见撕逼问题,就这么怼过去

2021-11-5 8:20:00

UI/UX

双11战袍揭秘,内附设计师穿搭指南

2021-11-5 10:43:07

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索