再两天 ~!!
在铁人赛的最後,我想要给各位带来的是噪声地形
的演算~
之所以想要写这个题目,原因是因为这个题目也可以承接我们上一篇讲的内容(透视投影),
而且还可以顺便带到一个在电脑图学中我觉得很有趣的概念:『噪声(Noise)
』。
不过我们这次还不会马上的带到主题的实作,而是会分成两篇
来进行。
今天的部分我们会先简单给大家科普一下『噪声(Noise)
』的概念,并且会有一个使用噪声
来做动画的实作范例。
接着就让我们开始吧~
能够熟练使用Photoshop
做後制的人一定有看过或用过下面这个玩意~
在Photoshop
中,这种花纹的图样被称为『云状效果(Cloud Effect)
』,而他其实也就是我们现在正要讲的『噪声(Noise)
』。
所以我们今天要讲的主题就这? 一张图?
Of course not.
噪声(Noise)
实际上是一种函数/演算法的统称,而这种函数/演算法的意义就在於『创造出有平滑、连续、有规律但却不循环的随机
』。
『创造出有平滑、连续、有规律但是却不循环的随机』这句话听起来可能很难懂,所以我们一步一步来解释~
一般情况下,我们在前端要产生随机数值,大多会使用Math.random()来产生一个介於0~1的数字,然後给订一个极小值,一个极大值,然後接着就在这个范围内,用内插法
的方式 取得一个随机数。
如果不记得内插法是什麽的话,可以回去翻翻国中课本XD,或是点这里
假设我们今天用for loop
去run 100圈
,也就是从x=0
到x=99
,然後每圈我们都用Math.random()*1000 抛出一个随机值y
那麽如果我们把这个结果画在x/y图
上,可能会长得像这个样子:
然後我们接着用线条把点连起来~
我们发现这些点形成了一个很崎岖而且随机的波形
这时候可能就会有人想:
有没有可能透过某种方式,去产生一个相对不这麽崎岖,但是同样是随机的波形呢?
当然有,而这个问题的解法之一就是我们所谓的噪声(Noise)
噪声(noise)
这种函数/ 演算法可以藉由传入座标值(可以是一维空间
,二维空间
甚至到多维空间
)而得到一个随机数,而这个随机数在 『传入的座标值为邻近座标』的情况下,会得到大小接近的值,所以像这样的函数就可能带来和缓但却随机的波形曲线
。
噪声
其实有很多种类型,主要是因为演算方式的不同,而会在波形规律的细节上有差异。而在电脑动画或电脑绘图的领域,最常出现的噪声就是『柏林噪声(Perlin's Noise)
』,它是当代科学家Ken Perlin
所发明的一种演算方法。
是的,这位老兄还活着,而且是个中年大叔,他不是古人~
噪声能被运用的范围很广,但是他在电脑绘图领域最常被使用的地方就在於创造自然的凹凸面
,像下面这个3D模型就是柏林噪声
的应用范例之一,而我们这次要介绍的案例也是要使用到柏林噪声
函数来做运算~
我们对於噪声
的介绍就差不多到这个地方,接着我们要来看看我们这次的实作~
如果对柏林噪声还有兴趣的话可以点这边
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沿着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 图表绘制服务
点击进入React源码调试仓库。 本篇是详细解读React DOM操作的第壹篇文章,文章所讲的内容发...
前言: 本篇为介绍逻辑运算子,并搭配if做解释。 逻辑运算子的短路特性: 若单看运算子左运算元,就可...
前言 参考 Tyler Potts 的 Demo 影片- Build a Music app usi...
这篇文章主要是在记录,celery 的任务状态以及该如何删除在任务伫列中的任务 有问题欢迎留言讨论喔...
二元搜寻树(Binary Search Tree)建立的方法 insert: 新增元素进入树中 de...