今天我們要進行煙霧動畫最後的部分。
昨天 p5.js 實戰演練(七) –– 煙霧動畫實作(二) 提到我們必須解決 noise function 震幅隨著半徑越來越大的問題,這原因其實很好理解,我們用 n * radius
來構成最後的極座標半徑 r
,n
的影響當然會隨著 radius
數值逐漸加大。
variance
解決方法是,我們讓 radius
的變化和變數 n
脫鉤,所以我們可以更改原本的函數參數,現在應該會有四個可控變因:
radius
(隨時間越來越大)variance
(基本上不隨時間改變)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);
}
}
現在我們試著只讓 radius
隨著時間增長,其他的參數維持不變,看看最後結果如何?
function setup() {
createCanvas(600, 600);
background(0);
}
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 * variance + 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 * variance + radius);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
function draw() {
background(0);
translate(width/2, height/2);
circular_smoke(-10 + frameCount*0.5, 100, 30, 1);
}
smoke_args_list
看起來效果好了不少,但現在還有兩個問題:
第一個問題很好解決,我們只需要讓 variance
隨著時間小幅增長就好了。
第二點和第三點是同一層面的問題,牽涉到煙霧的出現與消失,所以要改變直接呼叫 circular_smoke(-10 + frameCount*0.5, 100, 30, 1);
的方式,我打算維護一個 Object 的陣列 smoke_args_list
,每一個 Object 的內容代表要塞入 circular_smoke
的參數表,所以這個陣列的長度代表當下會渲染出的煙霧數量。
let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha
);
}
}
我們必須決定在某個固定的時間點增加煙霧的數量,假設要在每 150 幀增加一個煙霧動畫,那我們可以這樣處理:
let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
if (frameCount % 150 == 1) {
smoke_args_list.unshift({
radius: -10,
variance: 100,
line_num: 30,
alpha: 1
});
}
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha
);
}
}
然後針對外部參數做出時間上的變化:
radius
隨時間放大,讓煙霧暈開(每幀增加 0.5
)variance
隨時間放大,增加氣流擾動效果(每幀增加 0.1
)alpha
隨時間減小,製造淡出效果(每幀減小 0.002
)let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
if (frameCount % 150 == 1) {
smoke_args_list.unshift({
radius: -10,
variance: 100,
line_num: 30,
alpha: 1
});
}
for (let args of smoke_args_list) {
args.radius += 0.5;
args.variance += 0.1;
args.alpha -= 0.002;
}
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha
);
}
}
最後我們要逐一檢驗,是否 alpha
已經小於等於 0(已經不會在畫布上顯示),那我們就要將此參數 Object 從 smoke_args_list
中剔除:
let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
if (frameCount % 150 == 1) {
smoke_args_list.unshift({
radius: -10,
variance: 100,
line_num: 30,
alpha: 1
});
}
for (let args of smoke_args_list) {
args.radius += 0.5;
args.variance += 0.1;
args.alpha -= 0.002;
}
while (smoke_args_list.length > 0) {
if (smoke_args_list[smoke_args_list.length-1].alpha < 0) {
smoke_args_list.pop();
} else {
break;
}
}
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha
);
}
}
來看看最後渲染的結果:
function setup() {
createCanvas(600, 600);
background(0);
}
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 * variance + 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 * variance + radius);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
if (frameCount % 150 == 1) {
smoke_args_list.unshift({
radius: -10,
variance: 100,
line_num: 30,
alpha: 1
});
}
for (let args of smoke_args_list) {
args.radius += 0.5;
args.variance += 0.1;
args.alpha -= 0.002;
}
while (smoke_args_list.length > 0) {
if (smoke_args_list[smoke_args_list.length-1].alpha < 0) {
smoke_args_list.pop();
} else {
break;
}
}
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha
);
//circular_smoke(-10 + frameCount*0.5, 100 + frameCount*0.1, 30, 1);
}
}
看起來效果變的相當好了,但每個煙霧的 pattern 看起來都非常一致,我希望每個煙霧都有自己的變化,並且我們還希望在出現時有個淡入的效果,而不是直接冒出來,那我們可以這樣修改:
function setup() {
createCanvas(600, 600);
background(0);
}
function circular_smoke(radius, variance, line_num, alpha, seed) { // 增加隨機參數 seed
noFill();
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) {
// 在 noise function 的位置座標上加入隨機變數 seed
let n = float(noise(theta * 0.5, seed + i * 0.02, frameCount * 0.005));
if (theta == 0) {
head_n = n;
}
let r = float(n * variance + radius);
vertex(r * cos(theta), r * sin(theta));
} else {
// 在 noise function 的位置座標上加入隨機變數 seed
let n = float(noise(theta * 0.5, seed + 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 * variance + radius);
vertex(r * cos(theta), r * sin(theta));
}
}
endShape(CLOSE);
}
}
let smoke_args_list = [];
function draw() {
background(0);
translate(width/2, height/2);
if (frameCount % 150 == 1) {
smoke_args_list.unshift({
radius: -10,
variance: 100,
line_num: 30,
alpha: [0, 1], // 第二個參數若是 1 代表淡入階段,若是 0 代表淡出階段
seed: random()*1000 // 增加隨機參數 seed
});
}
for (let args of smoke_args_list) {
args.radius += 0.5;
args.variance += 0.1;
// alpha 參數增加淡入階段
if (args.alpha[1] == 1) {
args.alpha[0] += 0.02;
if (args.alpha[0] >= 1) {
args.alpha[1] = 0; // 切換為淡出階段
}
} else {
args.alpha[0] -= 0.002;
}
}
while (smoke_args_list.length > 0) {
if (smoke_args_list[smoke_args_list.length-1].alpha[0] < 0) { // 記得加上 [0]
smoke_args_list.pop();
} else {
break;
}
}
for (let args of smoke_args_list) {
circular_smoke(
args.radius,
args.variance,
args.line_num,
args.alpha[0],
args.seed // 增加隨機參數 seed
);
}
}
作品網址:
https://openprocessing.org/sketch/2315699
費盡千辛萬苦終於完成了,感謝大家費心閱讀,下個單元我們開始來講貝茲曲線。