再兩天 ~!!
在鐵人賽的最後,我想要給各位帶來的是噪聲地形
的演算~
之所以想要寫這個題目,原因是因為這個題目也可以承接我們上一篇講的內容(透視投影),
而且還可以順便帶到一個在電腦圖學中我覺得很有趣的概念:『噪聲(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 ~