【p5.js創作教學】CreativeCoding 花火大會(直播筆記)

說到夏天,就想到海邊;說到海邊,就想到日劇裡的西瓜跟煙火。一束束的煙火短暫但繽紛,燃燒自己的生命點燃絢麗的光譜,珍惜每次綻放都是不同的樣貌。最近(2021年8月)有一群日本Creative Coder在Processing Community Day時串連起社群,在Twitter上辦起虛擬的花火大會,透過各自的作品在版面上綻放了大大小小的煙火,替最近被疫情拉開實體距離的生活中,增添一些夏天的顏色。

讓我們抓住夏天的尾巴,一起用粒子系統與漸變顏色創作煙火吧 🎆

今天要使用的是OpenProcessing搭配p5.js函式庫的大禮包組合,如果對這兩個工具還不太熟悉在這篇文章可以看到更多介紹 👉🏻 p5.js 快速上手

讓我們用草稿規劃一下煙火的概念,如果要做以粒子為基礎、從中心炸開的煙火,應該是一顆粒子從畫面水平線的底部往上移動特定距離,在上方炸開很多不同的粒子、且粒子各自擁有不同的運動方向。

根據以上的概念,我們今天會切分為以下步驟來進行:

  1. 粒子系統
  2. 動態延伸(移動、爆炸分裂)
  3. 顏色變換
花火大會作品草稿示意圖
花火大會作品草稿示意圖

製作粒子系統

首先第一個步驟我們先完成煙火的核心——粒子系統,以單顆粒子的物理模型來說會有位置(P)、速度(V)、加速度(a)和顏色(Color)、大小(r)等變數。在OpenProcessing先把畫布設成1000×1000、黑色的夜空之後,另外新增一個Tab,用來放置我的們的Class particle,在初始化時我們希望引入一些變數args裡面帶入一些固定的參數做使用,如果使用者有特別設定,把使用者引入的參數args蓋到預設值def上,再把客製化後的設定值蓋到這個物件本體this上。

//Tab2
class Particle {
  constructor(args){
    let def = {
      p: createVector(0,0), //位置
      v: createVector(0,0), //速度
      a: createVector(0,0), //加速度
      color: color('red'), //顏色
      r: 10, //大小、半徑
    }
    Object.assign(def,args)
    Object.assign(this,def)
  }
}

接下來介紹兩個關鍵的method分別是draw()update(),分別負責顯示和更新,切分成兩個部分是為了在更新的時後不動到最初始的顯示,把邏輯層區分出來,這樣對模組化的製作與管理也比較容易。

在同一個 tab2,先來處理draw()push()會保留目前的drawing style、而pop()則會回復這些設定,兩個必須搭配使用。假設粒子移動到this.p位置、顏色this.color、尺寸是this.r

class Particle{
  ...
  draw(){
    //顯示
    push()
      noStroke()
      translate(this.p) //processing可以只給向量,不一定要x,y
      fill(this.color)
      circle(0,0,this.r)
    pop()
  }
  update(){
    //資料更新
  }
}

在主要的程式定義一個陣列particles把粒子都裝進去,我們來初始化一顆粒子試試看,讓objParticle根據剛剛的規範來製作,放在外面並用let比較不會有全域打架的問題objParticle = new Particle() ,成像的位置在畫布寬高一半處,這時在畫布中間就可以看到我們千辛萬苦的第一顆隨機色粒子啦。

let particles = []
let objParticle 
function setup() {
  createCanvas(1000, 1000);
  background(0);
  objParticle = new Particle({
    p: createVector(width/2,height/2),
    r: 100,
    color: color(random(255),random(255),random(255))
  })
}

function draw() {
  objParticle.update()
  objParticle.draw()
}
萬事起頭難,頭過身就過,黑夜中的一顆小紅點。
萬事起頭難,頭過身就過,黑夜中的一顆小紅點。

單一粒子動態軌跡

update()處理位置(P)、速度(V)、加速度(a)和大小(r)的變化,每一顆的位置都會加上速度、而速度都會加上加速度。

update(){
  //資料更新
  this.p.add(this.v)
  this.v.add(this.a)
  this.r*=0.993 //由大變小
}

有物理模型後,我們來處理速度(v)和加速度(a),這邊介紹一個函式random2D(),可以在vector上隨機產生一個2D的向量。套用到速度上隨機產生方向,在爆炸初始時粒子會往原先的方向衝再往下掉,乘5倍讓初始速度>加速度,設定加速度為0.1,這樣我們就得到單一粒子的運動軌跡,也就是煙火炸開時的單一根花瓣。

單獨一顆的粒子運動軌跡。
單獨一顆的粒子運動軌跡。

製作束狀粒子群

在陣列內紀錄產生的粒子,先draw完後再update產生動態。
在陣列內紀錄產生的粒子,先draw完後再update產生動態。

有了一個粒子後,我們可以來做一束的煙火,用這些粒子加起來做成陣列。我們先把objParticle拿進來,用for迴圈做出50個粒子objParticle,再push到陣列particles內。在draw的地方把清單一個一個抓出來,我們就得到初步的美麗煙火了。

let particles = []
 
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0) 
  rect(0,0,width,height)
	
  for(let i=0;i<50;i++){
		
    let objParticle = new Particle({
      p: createVector(width/2,height/2),
      v: p5.Vector.random2D().mult(5),
      a: createVector(0,0.1),
      r: 20,
      color: color(random(255),random(255),random(255))
    })
    particles.push(objParticle)
  }
}

function draw() {
  fill(0,5) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
}
在夜空中綻放的一束煙火,看起來有點像下垂的海葵(?)
在夜空中綻放的一束煙火,看起來有點像下垂的海葵(?)

模組化並自動發射

完成了一束煙火後,我們要接著做此起彼落發射的夏日花火祭,把發射的動作包成一個function firework就可以重複呼叫它。包起來後先在setUp呼叫一次,也可以引入位置參數(p),如果該參數有值就顯示、沒有則出現在畫面中央。接著設定他產生的頻率,每隔100個frame放一次煙火。

大家可以發現我們調高了煙火的數量,從50到100個,在這個情況中為了預防畫面因為生成的東西越來越慢,我們來消除超出畫面的煙火,用filter()留下小於畫面的物件。

仔細觀察煙火的粒子除了大小不同外,每顆的初始速度也不同,如果初始速度相同就會較規則,看起來像下垂的海葵(?),這邊用random()給予任意值處理,煙火的顏色也調整成HSB模式,相較於RGB模式有更彈性的明度暗度可以使用,色調的變數請參考下圖。

我們把HSB色調分為兩個部分:baseHue和Hue。BaseHue為固定的偏移量,hue為根據每個粒子隨機產生出的値。
我們把HSB色調分為兩個部分:baseHue和Hue。BaseHue為固定的偏移量,hue為根據每個粒子隨機產生出的値。
let particles = []

function firework(p){
  push()
	
    let baseHue = random(300)
	
    colorMode(HSB)
    for(let i=0;i<100;i++){
      let hue = random(0,120)
      let objParticle = new Particle({
        p: p || createVector(width/2,height/2), //有位置p時取用p,沒有時就從畫面中央
        v: p5.Vector.random2D().mult(random(1,10)),
        a: createVector(0,0.1),
        r: random(40),
        color: color((baseHue+hue)%360,360,360) //避免>360的數字都是紅色
      })
      particles.push(objParticle)
    }
  pop()
}

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0) 
  rect(0,0,width,height)
  firework()
}

function draw() {
  fill(0,5) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
  if (frameCount%100==0){
    firework()
  }
  particles = particles.filter(obj=>obj.p.y<height) //留下小於畫面的物件
	
  fill(0)
  rect(0,0,100,50)
  //計算畫面中粒子數
  fill(255)
  textSize(20)
  text(particles.length,50,50)
}
調整過後的煙火,終於看起來比較有層次感,不像下垂的海葵了。
調整過後的煙火,終於看起來比較有層次感,不像下垂的海葵了。

基礎版:滑鼠觸發煙火

接下來加入mouse的互動。首先註解掉自動產生的frameCount,每當滑鼠按壓時就在該位置呼叫firework,為了避免重複參數造成順序混亂,在呼叫時把p包成一個物件{p},記得setUp時也要回傳一個空的物件firework({})

這時候會產生一個問題,因為p引入firework被所有的粒子共用,所以有幾顆粒子他就會被update幾次,我們用copy()複製p出來給當下的粒子,避免所有的粒子共用位置。再加入fireRpraticleR等參數做出隨機粒子大小和隨機煙火大小。

function mousePressed(){
  firework({
    p: createVector(mouseX,mouseY),
    fireR: random(1,100), //煙火的大小
    particleR: random(1,10) //粒子的大小
  })
}
function firework({p, fireR, particleR}){
  push()
    let baseHue = random(300)
	
    colorMode(HSB)
    for(let i=0;i<100;i++){
      let hue = random(0,120)
      let objParticle = new Particle({
        p: (p && p.copy()) || createVector(width/2,height/2), //複製新的位置給當下的粒子,讓它重複100遍
        v: p5.Vector.random2D().mult(random(1,fireR || 5)), 
        a: createVector(0,0.1),
        r: particleR || random(40),
        color: color((baseHue+hue)%360,360,360)
      })
      particles.push(objParticle)
    }
  pop()
}
在夜空中用滑鼠點點點,我們就有初階的煙火大會囉
在夜空中用滑鼠點點點,我們就有初階的煙火大會囉~

進階篇:用聲音觸發煙火

做完基礎煙火互動後,如果可以用聲音來觸發煙火那一定很酷,p5.js裡有一些關於「聲音」相關的函式,今天介紹的是Mic Input可以截取電腦麥克風的聲音,我們先開啟p5.sound的library。

開啟p5.sound
開啟p5.sound

套用官方語法在setUp加上input = new p5.AudioIn()開始取用聲音。

let input

function setup() {
  createCanvas(1000,1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  firework({})
	
  input = new p5.AudioIn()
  input.start()
}

draw()加上觸發條件,如果有聲音,即在畫布上放煙火。這樣我們聲控的煙火大會就大功告成啦!

function draw() {
	
  let volume = input.getLevel()
  let speaking = voulume>0.15
	
  fill(0,8) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
  particles = particles.filter(obj=>obj.p.y<height) //留下小於畫面的物件
	
  fill(0)
  rect(0,0,400,200)
  fill(255)
  textSize(20)
  text(speaking,50,50)
	
  if (speaking){
    firework({
      p: createVector(mouseX,mouseY),
      fireR: random(1,5),
      particleR: random(1,10)
    })
  }
}

小試身手

做完上面的煙火後,這邊提供幾個大家可以繼續嘗試看看的方向,希望大家可以長出各式各樣的煙火,讓夏天的夜晚更為熱鬧!

  • 粒子的顏色漸層
    可以在def的地方新增endColor: color('yellow'),利用lerpColor()這個漸變函式在Update()指定顏色的變化跟階數。
this.color = lerpColor(this.color, this.endColor, 0.05) //每次變換0.05
  • 扭曲的粒子運動軌跡
    在粒子translate的時候如果根據sin/cos偏移,可以做出扭曲的煙火效果會更漂亮。
curve: random(5),
curveFreq: random(2,40),

translate(this.p.x+sin(this.p.y/this.curveFreq)*this.curve,this.p.y+cos(this.p.x/this.curveFreq)*this.curve)⁠
  • 製造煙火的霧氣
    透過在Particle中的draw()增加一些半透明且半徑較大的粒子,來增加模糊的光影。Color先複製一份避免動到原先的設定,用函示setAlpha()製作透明度。
let copyColor = color(this.color.toString())
  copyColor.setAlpha(10)
  for(var i=0;i<100;i+=10){ //重複畫圓形
  fill(copyColor)
  circle(0,0,this.r*i/20) ⁠
}
  • 混合模式
    加入混合效果,blendMode()調整顏色呈現。
push()
  blendMode(SCREEN)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
pop()
  • 效能處理
    設定當r小於某個半徑時就不顯現,減少效能負擔。
particles = particles.filter(obj=>
            obj.p.y<height &&
            obj.r>0.01
  )

以上就是這次的教學,成品請參考這裡,希望大家還玩得開心,那我們就下次再見啦!

也許你對互動生成式藝術比較有興趣?來看看老闆的《互動藝術程式創作入門》課程,跟著將近兩千位同學一起把程式碼當作畫筆創作,或是先看看這篇文章,欣賞同學們完成的作品吧!

此篇直播筆記由幫手 Jeudi Kuo 協助整理

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