這次的文章是新的創作應用單元,我們要來實作環狀煙霧的效果:

Imgur

主要用的是上一個單元 p5.js 基礎教學(八) –– noise 函數 使用的函數:

noise()

vertext()

這個環狀煙霧在視覺呈現上和前一個單元的彩帶動畫非常相似,其實各位可以把它理解為,把彩帶環成一圈就可以變成環狀煙霧了,程式上也是這樣實作的。

但彩帶頭尾的接法還是必須好好思考一下,畢竟 noise 函數在頭跟尾的座標值並不是「平滑」相連的平面。

直角座標與極座標

前一個單元的彩帶動畫,我們等於是在畫布上實作了一個 noise 平面函數,y 座標為該平面函數的 noise 函數數值,x 座標和深度值為該 noise 函數的位置座標。

Imgur

我們還拿了 noise 函數的二維灰階圖去做表格比較:

動畫類型
位置座標
noise 數值

彩帶
x 座標和深度值
y 座標

二維路面(二維灰階圖)
x, y 座標
灰度值

但這些都是在直角座標上呈現出來的 noise 平面,如果要把整個平面彎起來讓頭尾相連,也就是說讓這個 noise 數值,能夠以環形的效果呈現在畫布上,那我們就必須引入「極座標」的概念。

Imgur

在一個二維空間中標記一個點,我們可以用直角座標或是極座標系統來表示這個點的位置,直角座標是就我們最常見到的 (x, y) 標記形式,極座標的話,我們常用 (r, θ) 這兩個代號做表示,其中:

r 為該點到原點 (0, 0) 的距離,我們可以稱之為半徑。

θ 為該點與 (0, 0) 相連的直線和正向 x 軸的夾角。

這兩個系統都可以很好的表示二維平面上的每一個點位置,比較特別的是 θ 的範圍不像 rxy 必須是正負無限大,θ 只要在 0 ~ 2*pi 之間就足以標記平面上的所有的點。

Imgur

直角座標和極座標如何轉換呢?比如說在直角座標的 (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 < 2PI; theta += 2PI/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); } }

這是程式呈現出來的結果:

Imgur

這和我們之前預測的差不多,我們沒有辦法「平滑」的相接彩帶的頭部跟尾部,因為在頭部和尾部的 (r, theta) 並不相鄰,所以 noise 數值也彼此不相關。

那這裡我們就必需使用一些數學技巧,將兩側巧妙的連接起來。

使用 smoothstep 函數

smoothstep 是計算機圖學中用來進行插值的函數,主要用來生成一系列平滑過渡的值,我們要用這個函數在彩帶的頭部跟尾部來完成平滑相接。

smoothstep 函數的數學表示法為

Imgur

以下為函數圖:

Imgur

可以看到這個函數從 0 到 1 的過渡是非常平滑的,如果是用一般的線性插值:

Imgur

以下為函數圖:

Imgur

可以看出在 x = 0x = 1 的位置會有一個明顯的折點,就不是所謂的平滑過渡了。

那我們要怎麼把 smoothstep 函數用來進行頭尾相接呢?我的做法是,取最後 theta 在 11/12 * 2pi ~ 2pi 之間的部分,以 smoothstep 數值的比例與 theta 為 0 時的 noise 數值進行混合。

若我們以這種形式平滑的混合頭部跟尾部的 noise 數值,最後我們會得到以下結果:

Imgur

但因為今天的篇幅已經夠多了,所以先給讀者們一個回家作業,花時間想一下如何導入 smoothstep 函數改善既有程式,明天再公佈解答。