Day 29 - 3D绘图篇 - 噪声地形演算I - 成为Canvas Ninja ~ 理解2D渲染的精髓

再两天 ~!!

在铁人赛的最後,我想要给各位带来的是噪声地形的演算~

之所以想要写这个题目,原因是因为这个题目也可以承接我们上一篇讲的内容(透视投影),

而且还可以顺便带到一个在电脑图学中我觉得很有趣的概念:『噪声(Noise)』。

不过我们这次还不会马上的带到主题的实作,而是会分成两篇来进行。

今天的部分我们会先简单给大家科普一下『噪声(Noise)』的概念,并且会有一个使用噪声来做动画的实作范例。

接着就让我们开始吧~

什麽是噪声?

能够熟练使用Photoshop做後制的人一定有看过或用过下面这个玩意~

img

Photoshop中,这种花纹的图样被称为『云状效果(Cloud Effect)』,而他其实也就是我们现在正要讲的『噪声(Noise)』。

所以我们今天要讲的主题就这? 一张图?

Of course not.

噪声(Noise)实际上是一种函数/演算法的统称,而这种函数/演算法的意义就在於『创造出有平滑、连续、有规律但却不循环的随机』。

『创造出有平滑、连续、有规律但是却不循环的随机』这句话听起来可能很难懂,所以我们一步一步来解释~

一般情况下,我们在前端要产生随机数值,大多会使用Math.random()来产生一个介於0~1的数字,然後给订一个极小值,一个极大值,然後接着就在这个范围内,用内插法的方式 取得一个随机数。

如果不记得内插法是什麽的话,可以回去翻翻国中课本XD,或是点这里

假设我们今天用for loop去run 100圈,也就是从x=0x=99,然後每圈我们都用Math.random()*1000 抛出一个随机值y

那麽如果我们把这个结果画在x/y图上,可能会长得像这个样子:

img

然後我们接着用线条把点连起来~

img

我们发现这些点形成了一个很崎岖而且随机的波形

这时候可能就会有人想:

有没有可能透过某种方式,去产生一个相对不这麽崎岖,但是同样是随机的波形呢?

当然有,而这个问题的解法之一就是我们所谓的噪声(Noise)

img

噪声(noise)这种函数/ 演算法可以藉由传入座标值(可以是一维空间二维空间甚至到多维空间)而得到一个随机数,而这个随机数在 『传入的座标值为邻近座标』的情况下,会得到大小接近的值,所以像这样的函数就可能带来和缓但却随机的波形曲线

噪声其实有很多种类型,主要是因为演算方式的不同,而会在波形规律的细节上有差异。而在电脑动画或电脑绘图的领域,最常出现的噪声就是『柏林噪声(Perlin's Noise)』,它是当代科学家Ken Perlin所发明的一种演算方法。

是的,这位老兄还活着,而且是个中年大叔,他不是古人~

噪声能被运用的范围很广,但是他在电脑绘图领域最常被使用的地方就在於创造自然的凹凸面,像下面这个3D模型就是柏林噪声的应用范例之一,而我们这次要介绍的案例也是要使用到柏林噪声函数来做运算~

img

我们对於噪声的介绍就差不多到这个地方,接着我们要来看看我们这次的实作~

如果对柏林噪声还有兴趣的话可以点这边

噪声在Canvas绘图的实际运用案例

github repo: https://github.com/mizok/ithelp2021/blob/master/src/js/silky-wave/index.js

github page: https://mizok.github.io/ithelp2021/silky-wave.html

光看影片可能不好理解这个动画是怎麽实作出来的,所以我们一步一步讲

这个动画主要的部分就是核心方法 - drawAll()

drawAll()是一个在每一圈RAF都会运作的渲染方法,他在一帧内执行的内容大概是这样

  • 把整张canvas填满config中设定的背景色
  • 如我们所见,这个案例其实是由很多波动的线条所构成的,而在这个方法中我们会有两层回圈,外面那圈会决定一条线条的颜色深浅,然後里面那圈则是负责画波形线条

外面那圈回圈基本还算好理解,所以这边会把重点放在里面那圈回圈。

首先这行的用意在於把一整个canvas沿着x轴方向分割成很多部分来执行,这个就是波形的成因(波形其实是由很多细小的折线所构成的~)

for (let x = 0; x < this.cvs.width + this.config.vertexGap; x += this.config.vertexGap)

然後接着重点就是这部分

let randomNoise = perlinNoise(x * this.config.horizontalNoiseParameter, i * this.config.verticalNoiseParameter, this.frameCount * this.config.frequency);
        let y = linearInterpolation(randomNoise, 0, 1, 0, this.cvs.height);

randomNoise会用柏林噪声的函数回传一个浮点数,而这个浮点数是藉由输入x,y,z值来得到的。
我们在这边输入的x会是内圈for loop在迭代时取得的canvas片段位置座标。
y值是外圈for loop在迭代时使用的变数i。
最後z值则是动画当下已经执行的总帧数。

我们刚刚有说过柏林噪声的函数,会因为传入的座标位置相邻,而产生相似的值。

假设今天内圈跑完行程(也就是外圈执行一次的情况),我们可以发现内圈行程的每圈:

传入的x: 基本不相同(因为x座标会迭代)
传入的y: 基本相同(因为迭代的i没有变)
传入的z: 基本相同(因为是同一帧)

而这样内圈跑了一圈之後,我们就会的到一连串相邻的x/y/z座标值(只有x不一样),而我们把他丢到柏林噪声的函数中,就会得到一连串近似的浮点数,最後我们再把这一连串的浮点数拿去乘以canvas的高,就得到了一连串位於canvas的y座标,然後再搭配迭代的x值,就会形成一条波形。

然後接着外圈执行第二次,内圈执行第一圈,这时我们会发现:

传入的x: 跟之前外圈执行第一次,内圈执行第一圈时一样(因为x座标归0了)
传入的y: i 因为外圈是跑第二次所以+1了
传入的z: 基本相同(因为还是同一帧)

我们会发现在外圈执行第二次时,所得到的浮点数都会略比上一批第一圈得到的浮点数多(少)一点点,这样结果就造就了一条位置略偏差一咪咪的波形曲线。

後面我应该就不用讲了吧~

大致上原理就是这样了,这边我再把drawAll方法的源码补上,给各位方便对照~


drawAll() {
    
    this.background(this.config.bgColor);
    for (let i = 0; i < this.config.range; i++) {
    
       //定义单一线条颜色
      let thisLineAlpha = linearInterpolation(i, 0, this.config.range, 0, 1); 
      this.ctx.strokeStyle = `rgba(255,255,255,${thisLineAlpha})`;
      this.ctx.globalAlpha = this.config.globalAlpha;
      //把水平座标分割成复数段落
      for (let x = 0; x < this.cvs.width + this.config.vertexGap; x += this.config.vertexGap) {
        let randomNoise = perlinNoise(x * this.config.horizontalNoiseParameter, i * this.config.verticalNoiseParameter, this.frameCount * this.config.frequency);
        let y = linearInterpolation(randomNoise, 0, 1, 0, this.cvs.height);
        if (x === 0) {
          this.ctx.beginPath();
          this.ctx.moveTo(x, y);
        }
        else if (x < this.cvs.width + (this.config.vertexGap / 2)) {
          this.ctx.lineTo(x, y, x, y + 100)
        }
      }
      this.ctx.stroke();
    }
    requestAnimationFrame(this.drawAll.bind(this))
  }

小结

这次我们示范了柏林噪声在canvas渲染中实际的运用案例,下一次我们就要进入主题: 『噪声地形』实作了~

敬请期待 :D ~


<<:  【Day 29】Google Apps Script - 延伸篇 - Google sites 协作平台与 Charts Service 图表绘制服务

>>:  Day 30:Ansible Role

React和DOM的那些事-节点删除算法

点击进入React源码调试仓库。 本篇是详细解读React DOM操作的第壹篇文章,文章所讲的内容发...

为了转生而点技能-JavaScript,day5(Falsy、Truthy、&&、||、!

前言: 本篇为介绍逻辑运算子,并搭配if做解释。 逻辑运算子的短路特性: 若单看运算子左运算元,就可...

[番外] 来个音乐拨放器 Play! (序)

前言 参考 Tyler Potts 的 Demo 影片- Build a Music app usi...

Python & Celery 学习笔记_删除任务

这篇文章主要是在记录,celery 的任务状态以及该如何删除在任务伫列中的任务 有问题欢迎留言讨论喔...

【Day16】[资料结构]-二元搜寻树Binary Search Tree-实作

二元搜寻树(Binary Search Tree)建立的方法 insert: 新增元素进入树中 de...