【p5.js創作教學】Aqua Planet 水色星球 – 來製作發光碰撞的行星吧!(直播筆記)

夜晚抬頭望向天空,黑夜中有著數不盡的星星,讓人不禁讚嘆太空之美。今天老闆就要化身為太空人,觀察一顆由冷暖色調所搭配而成的星球。

一開始會先使用函式的方式來建構藍色粒子雲,隨後加上 noise 來形成動態效果,並加上橘色系的粒子來做搭配。建構好粒子後,便在粒子四周增加弧形的線條,圍繞著粒子雲,最後則是繪製尺標,讓整體作品看起來更有正在觀測的感覺。

《Aqua Planet 水色星球》完成品
《Aqua Planet 水色星球》完成品

透過此次互動藝術創作教學,你會學到

  • 學習函式的使用方式,如何透過參數的傳遞,來指定粒子雲的位置與半徑大小範圍
  • 如何應用極座標的概念,來繪製粒子的運動位置,並且使用 noise 的技巧讓粒子隨機移動
  • 透過不同色彩疊加的模式,來達成每項物件所期待產生的視覺效果

在開始製作之前你該知道的p5.js小技巧

就讓老闆帶你們一步步完成吧

《Aqua Planet 水色星球》步驟拆解圖
《Aqua Planet 水色星球》步驟拆解圖

1. 建立粒子系統

首先,先從建立最核心的粒子開始,老闆建立了planet(),並指定了三個參數,由前到後分別是 X 位置、Y 位置以及距離中心點位置的距離 r,可以注意到這邊的 r 在函式中給定了預設的30,這代表當我們呼叫函式的時候,如果第三個數字是沒有給定的,那 r 就會預設成 30,而目前呼叫的函式是有給定值 40 的。

在放置粒子的座標上,老闆將座標移動至畫布的中間,以中間向四周繪製大小、顏色不一的藍色粒子。顏色的部分,老闆到 https://coolors.co/ 網站選取想要的顏色,並複製網址後段色碼的區域,再將文字處理成可使用的色碼形式。

ellipse 的參數前兩項是位置,第三個是半徑,所以 ellipse(random(r), random(r), rr) 所代表的是「以畫布的中心點起,隨機在距離0到40之間的位置上,繪製半徑介於0到20之間的圓」。

//給定顏色
var colorsBlue = "04080f-507dbc-a1c6ea-bbd1ea-dae3e5".split("-").map(a=>"#"+a)

// 建立 planet 函式來繪製粒子
function planet(x,y, r=30){
  push()
    translate(x,y)
    for(var i=0;i<30;i++){
      // 隨機給定顏色
      let cc= random(colorsBlue) 
      fill(cc)
			
      // 繪製陰影
      drawingContext.shadowColor = color(cc)
      drawingContext.shadowBlur =30
  
      // 設定半徑範圍,並繪製粒子
      let rr = random(0,20)
      ellipse(random(r), random(r), rr)
    }
  pop()
}

function setup() {
  createCanvas(800, 800)
  background(0)
	
  // 混和模式設定成 SCREEN
  blendMode(SCREEN)
  planet(width/2, height/2, 40)
}

function draw() {
  // ellipse(mouseX, mouseY, 20, 20);
}
1. 建立粒子系統
1. 建立粒子系統

2. 用 noise 建立連續變化

首先,先將繪製粒子的 planet() 移動至 draw() 裡面,讓粒子可以隨時間不斷變化繪製位子。

老闆覺得目前粒子在分布上似乎不太均勻,所以打算改用極座標來做,使用極座標來設定位置的方式在其他作品裡也曾使用過,可以參考 【p5.js創作教學】來畫一個瘋狂的龍捲風吧!(直播筆記) 這篇文章。

為了讓上一個與下一個粒子的分布上不要呈現完全隨機跳動,所以這裡老闆使用了 noise 來設定粒子的距離 xx、角度 ang 以及半徑大小rr

而在設定上,第一眼看上去會覺得「這一串程式碼好長喔」,但其實拆解下來,可以看到老闆只用了幾個參數就產生隨機,分別是代表粒子序號的 i、系統變量(也就是計算程式啟動以來的幀數 frameCount),以及當滑鼠水平移動時也會產生影響的mouseX

而其他的常數則是老闆慢慢嘗試去調整出來的,可以理解成是每一個常數所影響占比,所除的數字越大,該變數所影響的量越小。你們也可以自己改變數字,試出最喜歡的樣子。

這裡要特別注意的是,老闆希望整個粒子雲可以呈現中間密外圍疏,所以在 xx 這個設定距離的參數上的尾端又多乘上一個 noise。此外,在星球顏色的變化上也改為使用 noise,會顯得更加地平順,不會一閃一閃的。

var colorsBlue = "04080f-507dbc-a1c6ea-bbd1ea-dae3e5".split("-").map(a=>"#"+a)

function planet(x,y,r=30){
  push()
    translate(x,y)
    for(var i=0;i<200;i++){
      // 顏色改為連續變化,這樣才不會閃閃的
      let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
      fill(cc)
			
      drawingContext.shadowColor = color(cc)
      drawingContext.shadowBlur =30
			
      // 以極座標方式畫圓形位置
      // xx 乘上 noise,就會產生中間有比較多粒子的效果
      let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2 
      let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
      let rr = noise(i,500,frameCount/50+mouseX/500)*40 
      ellipse(xx*cos(ang),xx*sin(ang),rr)
    }
  pop()
}

function setup() {
  createCanvas(800, 800)
  background(100)
  // planet(width/2, height/2, 100)
}

function draw() {
  blendMode(BLEND)
  // 加上外框
  fill('#0f1954')
  rect(0,0,width,height)
    
  blendMode(SCREEN)
  planet(width/2, height/2, 400) 
}
2. 用 noise 建立連續變化
2. 用 noise 建立連續變化

3. 加上暖色粒子與調整粒子大小

目前的畫面有些單調,所以想加上一些偏橘色粒子。由於都是粒子,增加的程式碼就直接寫在 planet 函式中,而且我們可以複製原本藍色粒子的片段去做修改,讓整體視覺變成冷暖色交替。但是如果只有更改顏色、粒子大小太相近的話,看上去有點喧賓奪主,所以稍微調整成藍色大橘色小,將橘色粒子的半徑大小改為 rr/2

除了不同顏色大小的調整外,在粒子半徑大小 rr 的設定上,老闆為了使離中心越遠的粒子顯得越小,加上了開平方 sqrt

/* 加上暖色色票 */
var colorsRed = "fe7f2d-fcca46-a1c181-619b8a-333".split("-").map(a=>"#"+a)

function planet(x,y,r=30){
  push()
    translate(x,y)
    for(var i=0;i<200;i++){
      let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
      fill(cc)
			
      drawingContext.shadowColor = color(cc)
      drawingContext.shadowBlur =20
			

      let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2 
      let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
      /* 加上 sqrt,讓靠近中心的粒子較大,越遠的粒子較小 */
      let rr = noise(i,500,frameCount/50+mouseY/500)*30*(15/(sqrt(xx)+1))
      ellipse(xx*cos(ang),xx*sin(ang),rr)
			
      /* 加上橘球 */
      let cc2 = colorsRed[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length]
      fill(cc2)
			
      drawingContext.shadowColor = color(cc2)
      drawingContext.shadowBlur =10
      ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)	
    }
  pop()
}
3. 加上暖色粒子與調整粒子大小

4. 加上線條與修正藍色粒子

這裡要來繪製類似魔法陣的線條,連接方式是將每一個粒子的位置相連接起來。

為了可以記錄上一個粒子現在粒子的位置來做連接,必須要新增變數lastXlastRlastAng 來記錄上一個粒子的位置,在函式中記得也要更新此次粒子的位置,來作為下一個粒子的參考位置。

然而,將所有點與之間都連接起來,線條太多了,所以老闆增加了 random()<0.1 的條件,使線條出現的頻率不會那麼高。

除了魔法陣外,老闆也繪製了弧形的線條,原本想要使用 line 一段一段繪製,但是發現這樣實在有些麻煩,所以就改以使用 arc 的方式來畫。與線條一樣,透過加上random() 的方式,讓畫面呈現一閃一閃的效果 。

在藍色粒子上,由於陰影有一點太不亮了,所以把陰影跟粒子分開來調整,另外增加了shadowCC 來設定陰影。

function planet(x,y,r=30){
  push()
    translate(x,y)
    /* 1. 設定變數做紀錄,以便於繪製點與點的連線 */
    let lastX, lastR, lastAng
		
    for(var i=0;i<200;i++){
      /* 2. 將藍色球跟其陰影分開設定透明度 */
      let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
      cc.setAlpha(150)
      fill(cc)
			
      let shadowCC = color(cc) 
      shadowCC.setAlpha(255) 
      drawingContext.shadowColor = shadowCC; 
      drawingContext.shadowBlur =20;
				
      let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2 
      let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
      let rr = noise(i,500,frameCount/50+mouseY/500)*30*(15/(sqrt(xx)+1))
      ellipse(xx*cos(ang),xx*sin(ang),rr)
	
      let cc2 = colorsRed[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length]
      fill(cc2)
	
      drawingContext.shadowColor = color(cc2);
      drawingContext.shadowBlur =10;
      ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)
				
      /* 3. 新增點與點連線 */
      if (lastX && random()<0.1){
        push()
          stroke(255,50)
	  line(xx*cos(ang),xx*sin(ang),lastX*cos(lastAng),lastX*sin(lastAng))
	pop()
      }
				
      /* 4. 新增外圍弧形線條 */
      if (random()<0.5){
        push()
          stroke(255,50)
          noFill()
          arc(0,0,xx,xx,ang,ang+PI)
        pop()
      }
				
      /* 5. 更新變數 */
      lastX=xx
      lastR=rr
      lastAng=ang
    }
  pop()
}
4. 加上線條與修正藍色粒子

5. 調整平衡,加上材質

做到一個階段,有雛型的樣子後,老闆習慣為畫面加上材質,新增材質共有三大步驟,分別是命名材質、設定材質、疊加材質。

  • 命名材質
    在全域中命名材質變數 let overAllTexture
  • 設定材質
    setup() 中設定材質的樣式
overAllTexture=createGraphics(width,height)
overAllTexture.loadPixels()
// noStroke()
for(var i=0;i<width+50;i++){
  for(var o=0;o<height+50;o++){
    overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,10,20])))
  }
}
overAllTexture.updatePixels()
  • 疊加材質
    在 draw() 中改變與色塊的混合模式
push()
  blendMode(MULTIPLY)
  image(overAllTexture,0,0)
pop()

新增好材質後,接下來要來調整一些粒子跟線條的細節,目前所繪製的球體有 200個,這裡我們改為 120個,看起來比較不擁擠,另外粒子雲發散的距離從400改為300,縮小其範圍。這時候會發現,旁邊的弧形線條都擠在最裡面的位置了,所以在 arc 的參數上都乘上 2 ,讓弧形向外擴長,同時也加上橘色色系在線條上,讓整個作品看起來更加一致。還有一點細節是老闆在此加上了 noise,這使線條多了動態感。

調整到這裡,老闆覺得中間的粒子與線條差不多了,所以開始嘗試往背景的著手。這裡老闆使用 Pow 畫成由內而外,根據距離中心點的不同,疊加了好幾層不同透明度的圓,形成了一圈一圈的圓形尺規,雖說是尺規,但不是規規矩矩一圈一圈等距的圓形,這邊可以先觀察下圖兩者的差異。

等差常數與Pow的比較

Pow 其實就是以前高中數學所學過的指數,必須有兩個參數 Pow(底數, 次方),而老闆所使用的次方數是小於 1 的,這樣所產生的效果是,數值越小時,轉換過後所產生的差異會越大。舉例子來說,pow(1000,0.9) – pow(900,0.9) = 46 < pow(200,0.9) – pow(100,0.9) = 46,所以仔細觀察可以看到,越內圈的間距是大於外圈之間的間距,而這樣小細節,讓所疊加的圓並非死板板的一格一格,而是更加產生更加靈活一點的漸層感。

/*1. 上下的半徑壓扁,並將顏色修改為跟橘球一樣 */
if (random()<0.5){
  push()
    stroke(cc2)
    noFill()
    arc(0,0,xx*2,xx*2,ang*2,ang*2+noise(i,frameCount/200))
  pop()
}


function draw() {
  blendMode(BLEND)
	
  fill(30, 35, 86, 50)
  rect(0,0,width,height)
  blendMode(SCREEN)
   /*2. 修正半徑範圍  */
  planet(width/2, height/2, 300) 
	
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
	
  /*3. 加上圓形尺規  */
  stroke(255,50)
  noFill()
  blendMode(MULTIPLY)
  for(var i=0;i<width;i+=100){
    fill(150,map(i,width/2,width,0,20))
    ellipse(width/2,height/2,pow(i,0.9)*3,pow(i,0.9)*3)
  }
}
5. 調整平衡,加上材質

6. 加上漸層陰影與刻度

老闆希望那些橘色的小粒子可以多一些稜角,以搭配中心圓形的球,所以將橘色小粒子從 ellipse 改為 rect ,並且跟著軌跡去做旋轉,才不會那麼地死板。

//ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)
/*將橘色粒子的球體改為方形 */
push()
  rectMode(CENTER)
  translate(xx*cos(ang*2),xx*sin(ang*2))
  rotate(ang*2)
  rotate(i)
  rect(0,0,sqrt(rr)*sin(frameCount/2+i)*2)
pop()

接下來就來畫一些橫軸及縱軸的尺規,來讓畫面上扭曲沒有固定的形體可以透過 2D 固定的線條搭配。這裡繪製出的是每隔五格會有一條相對長一點的線條,這樣看去上會更加有刻度感。

/* 加上尺規 */
blendMode(BLEND)
stroke(colorsRed[1])
  push()
    for(var i=0;i<width;i+=10){
      line(i,40,i,45+(i/10%5==0?15:0))
      line(40,i,45+(i/10%5==0?15:0),i)
      point(i,i)
    }
  pop()

最後老闆本來想要外面加上一整個外框來框住星球,有一種正在觀測星球的感覺,但感覺有些太死板,所以嘗試了一些半圓形或是扇形的形狀,最終決定在右下角加上五個弧狀的扇形作為點綴,並與前面繪製弧型一樣,使用了 arc 來畫,並且使用了 noise 來增加隨機動態感。

/*右下角加上點綴型外框 */
noFill()
strokeWeight(5)
drawingContext.shadowColor = colorsRed[1];
drawingContext.shadowBlur =30;
for(var i=0;i<5;i++){
  let aa =  noise(i,frameCount/10)*2
  arc(width/2,height/2,width-100-i*20,height-100-i*20,aa,aa + noise(i,frameCount/10)/2)
}
  drawingContext.shadowBlur =0;

  strokeWeight(2)
6. 加上漸層陰影與刻度即完成
6. 加上漸層陰影與刻度即完成

結語

我們總結一下這次的水色星球小品,製作過程可以分為三大部分:

  1. 粒子的繪製是此次作品的核心,學習到了如何使用函式來讓程式更加有彈性。當一開始繪製好藍色粒子後,可以直接複製部分原本的程式碼修改,不需要重新撰寫。另外在粒子的位置上,可以觀察到當老闆在使用極座標繪製粒子時,在不同地方都透過細節上的變化來增加豐富度,像是為了使粒子雲可以呈現中間密外圍疏,所以距離設定上增加了 noise,亦或是根據離中心不同距離而有不同的粒子大小所增加的 sqrt 等。
  2. 弧形與線條連接 – 透過新增變數來紀錄上一個點現在這個點之間的位置,藉此能夠將粒子之間以線來做連接,以及透過 arc 來繪製圍繞在粒子雲四周的弧形、軌跡,增添細節。
  3. 視覺上的調整,除了加入材質外,也新增了尺規與座標,這樣固定的物件,與中心的動態物件做為對比,視覺上在穩定與動態之間達到平衡。

這就是我們用p5.js寫出來的簡單的小作品啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。

相同的繪製原理還能應用在甚麼作品上呢?若對p5.js寫成的互動藝術程式創作有興趣,歡迎加入老闆開的Hahow互動藝術程式創作入門課程,與另外將近兩千位同學一起創作吧!

此篇直播筆記由幫手 阮柏燁 協助整理

墨雨設計banner
分享
PHP Code Snippets Powered By : XYZScripts.com