上一個單元 p5.js 實戰演練(六) –– 煙霧動畫實作(一) 我們留了一個回家作業給大家,看要怎麼使用 smoothstep
函數來進行緞帶頭尾的接合。
先來上個解答。
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();
let head_n; // 用來儲存 theta 為 0 的 noise value
for (let theta = 0; theta < 2*PI; theta += 2*PI/180) {
// 用 if 判斷式區分是否為最後的 1/12 的段落
if (theta < 2*PI * 11/12) { // 使用原本邏輯
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n; // 儲存 theta 為 0 的 noise value
}
let r = float(n * 200);
vertex(r * cos(theta), r * sin(theta));
} else { // 用 smoothstep 混合原本的 n 和 head_n
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
ratio = 3*ratio*ratio - 2*ratio*ratio*ratio; // 使用 smoothstep 計算新的 ratio
let mixed_n = head_n + (n-head_n) * ratio;
let r = float(mixed_n * 200);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
呈現出來的結果:
主要更動的部份已經有用註解去做註記了,我針對一些比較重要的部分進行說明:
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
let r = float(n * 200);
vertex(r * cos(theta), r * sin(theta));
這個是原本的繪製邏輯,但在最後 theta 為 0
和 theta 為 2*pi
的時候,noise value 並不連貫,所以會導致連接的時候出現斷層,所以我們將最後 theta 在 11/12*2*pi ~ 2*pi
之間的部分改成:
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;
let mixed_n = (1-ratio) * head_n + ratio * n;
let r = float(mixed_n * 200);
vertex(r * cos(theta), r * sin(theta));
原本的計算半徑 r
的 noise value n
,改成變數 mixed_n
,這個是由變數 n
和變數 head_n
經由某個比例混合而成的,那我們要如何取這個比例呢?
這裡注意到 let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
,在 p5.js 中 map
函數用來將一個數值從一個範圍重新映射到另一個範圍:
map(value, ori_start, ori_end, target_start, target_end)
簡單來說就是查看 value
在範圍 ori_start ~ ori_end
之間的哪個位置,比如說是在 ori_start ~ ori_end
之間的三分之一段的位置:
那輸出就會映射到 target_start ~ target_end
之間同樣三分之一段的數值點:
map
函數用在這真的是再適合不過了!我們可以藉由 map
函數的映射,輕鬆得得出目前的 theta 在 11/12*2*pi ~ 2*pi
中的那一個區段:
這裡注意到我們將映射的範圍顛倒過來,變成 1 ~ 0
而不是 0 ~ 1
,因為越靠近 11/12*2*pi
,變數 n
的混合比例就要越高(越趨近於 1
),越靠近 2*pi
,比例就越低(越趨近於 0
)。
然後我們要導入 smoothstep
函數,讓混合的比例可以更加平滑。
ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;
假設我們不這樣做,直接用 1 ~ 0
的線性比例混合,會導致以下結果:
可以看到 theta 在 11/12*2*pi
和 2*pi
有明顯的摺痕出現。
在 p5.js 實戰演練(六) –– 煙霧動畫實作(一),我們已經展示了最後的成果,由多個環狀煙霧以同心圓的方式向周圍暈開,所以我們要將環狀煙霧抽象化為一個函數,以便能在程式中重複使用。
先來想想要將煙霧的哪些特性抽出來作為可調整的函數參數:
radius
(隨時間越來越大)line_num
(決定煙霧厚度)alpha
(先淡入再淡出)function circular_smoke(radius, variance, line_num, alpha) {
noFill();
strokeWeight(2);
for (let i = 0; i < line_num; i++) {
stroke(255 * i / line_num * alpha);
beginShape();
let head_n;
for (let theta = 0; theta < 2*PI; theta += 2*PI/180) {
if (theta < 2*PI * 11/12) {
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n;
}
let r = float(n * radius);
vertex(r * cos(theta), r * sin(theta));
} else {
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;
let mixed_n = (1-ratio) * head_n + ratio * n;
let r = float(mixed_n * radius);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
抽象化的過程非常簡單,接下來試著讓半徑隨著時間放大,看會有什麼樣的效果:
function setup() {
createCanvas(600, 600);
background(0);
}
function circular_smoke(radius, line_num, alpha) {
noFill();
strokeWeight(2);
for (let i = 0; i < line_num; i++) {
stroke(255 * i / line_num * alpha);
beginShape();
let head_n;
for (let theta = 0; theta < 2*PI; theta += 2*PI/180) {
if (theta < 2*PI * 11/12) {
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n;
}
let r = float(n * radius);
vertex(r * cos(theta), r * sin(theta));
} else {
let n = float(noise(theta * 0.5, i * 0.02, frameCount * 0.005));
let ratio = map(theta, 2*PI * 11/12, 2*PI, 1, 0);
ratio = 3*ratio*ratio - 2*ratio*ratio*ratio;
let mixed_n = (1-ratio) * head_n + ratio * n;
let r = float(mixed_n * radius);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
function draw() {
background(0);
translate(width/2, height/2);
circular_smoke(200 + frameCount*0.1, 30, 1);
}
看起來好像沒有達到理想的效果,當半徑加大,noise function 的振幅也逐漸加大,環狀帶寬也變得越來越大。
根據最終結果我們想要的,應該是煙霧隨時間暈開薄化的效果(振幅應該隨著半徑加大仍然維持不變,甚至是要變小)。
最後的優化流程留到明天繼續解說。