這次的文章是新的創作應用單元,我們要來實作環狀煙霧的效果:
主要用的是上一個單元 p5.js 基礎教學(八) –– noise 函數 使用的函數:
noise()
vertext()
這個環狀煙霧在視覺呈現上和前一個單元的彩帶動畫非常相似,其實各位可以把它理解為,把彩帶環成一圈就可以變成環狀煙霧了,程式上也是這樣實作的。
但彩帶頭尾的接法還是必須好好思考一下,畢竟 noise
函數在頭跟尾的座標值並不是「平滑」相連的平面。
前一個單元的彩帶動畫,我們等於是在畫布上實作了一個 noise 平面函數,y
座標為該平面函數的 noise 函數數值,x
座標和深度值為該 noise 函數的位置座標。
我們還拿了 noise 函數的二維灰階圖去做表格比較:
動畫類型 | 位置座標 | noise 數值 |
---|---|---|
彩帶 | x 座標和深度值 | y 座標 |
二維路面(二維灰階圖) | x, y 座標 | 灰度值 |
但這些都是在直角座標上呈現出來的 noise 平面,如果要把整個平面彎起來讓頭尾相連,也就是說讓這個 noise 數值
,能夠以環形的效果呈現在畫布上,那我們就必須引入「極座標」的概念。
在一個二維空間中標記一個點,我們可以用直角座標或是極座標系統來表示這個點的位置,直角座標是就我們最常見到的 (x, y)
標記形式,極座標的話,我們常用 (r, θ)
這兩個代號做表示,其中:
r
為該點到原點 (0, 0)
的距離,我們可以稱之為半徑。θ
為該點與 (0, 0)
相連的直線和正向 x 軸的夾角。這兩個系統都可以很好的表示二維平面上的每一個點位置,比較特別的是 θ
的範圍不像 r
、x
、y
必須是正負無限大,θ
只要在 0 ~ 2*pi
之間就足以標記平面上的所有的點。
直角座標和極座標如何轉換呢?比如說在直角座標的 (3, 3)
這個位置轉變成極座標就變成 (3*2^0.5, pi/4)
。
所以回到我們的 noise 平面函數,如果我們要呈現環狀的 noise 平面,我們可以將 r
座標設為該平面函數的 noise 函數數值,θ
座標和深度值設為該 noise 函數的位置座標。
動畫類型 | 位置座標 | noise 數值 |
---|---|---|
彩帶 | x 座標和深度值 | y 座標 |
二維路面(二維灰階圖) | x, y 座標 | 灰度值 |
環狀煙霧 | θ 座標和深度值 | r 座標 |
理解概念後我們就可以開始實作了。
這是原本的彩帶動畫程式:
function setup() {
createCanvas(600, 600);
background(0);
}
function draw() {
background(0);
noFill();
strokeWeight(2);
for (let i = 0; i < 50; i++) {
stroke(255 * i / 50);
beginShape();
for (let x = 0; x <= 600; x += 20) {
let n = float(noise(x * 0.001, i * 0.01, frameCount * 0.01));
let y = float(600 * n);
vertex(x, y);
}
endShape();
}
}
然後試著用極座標 r, theta
取代直角座標變數 x, y
的角色,並且依照前面的表格對應,將彩帶環成一圈,最後再調整一些細節參數。
function setup() {
createCanvas(600, 600);
background(0);
}
function draw() {
background(0);
noFill();
strokeWeight(2);
// 將座標原點移至中心點
translate(width/2, height/2);
for (let i = 0; i < 30; i++) {
stroke(255 * i / 30);
beginShape();
// for (let x = 0; x <= 600; x += 20) {
// 變數 x 改成 theta
for (let theta = 0; theta < 2*PI; theta += 2*PI/180) {
// let n = float(noise(x * 0.001, i * 0.01, frameCount * 0.01));
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
// let y = float(600 * n);
// vertex(x, y);
// 變數 y 改成變數 r
let r = float(n * 200);
vertex(r * cos(theta), r * sin(theta)); // 將極座標變數轉換成 xy 變數放到 vertex 函數
}
// 記得 endShape 要 CLOSE
endShape(CLOSE);
}
}
這是程式呈現出來的結果:
這和我們之前預測的差不多,我們沒有辦法「平滑」的相接彩帶的頭部跟尾部,因為在頭部和尾部的 (r, theta)
並不相鄰,所以 noise 數值也彼此不相關。
那這裡我們就必需使用一些數學技巧,將兩側巧妙的連接起來。
smoothstep
是計算機圖學中用來進行插值的函數,主要用來生成一系列平滑過渡的值,我們要用這個函數在彩帶的頭部跟尾部來完成平滑相接。
smoothstep
函數的數學表示法為
以下為函數圖:
可以看到這個函數從 0 到 1 的過渡是非常平滑的,如果是用一般的線性插值:
以下為函數圖:
可以看出在 x = 0
和 x = 1
的位置會有一個明顯的折點,就不是所謂的平滑過渡了。
那我們要怎麼把 smoothstep
函數用來進行頭尾相接呢?我的做法是,取最後 theta 在 11/12 * 2pi ~ 2pi
之間的部分,以 smoothstep
數值的比例與 theta 為 0 時的 noise 數值進行混合。
若我們以這種形式平滑的混合頭部跟尾部的 noise 數值,最後我們會得到以下結果:
但因為今天的篇幅已經夠多了,所以先給讀者們一個回家作業,花時間想一下如何導入 smoothstep
函數改善既有程式,明天再公佈解答。