Processing 案例 | 用粒子系统谱写冰与火之歌
文章目錄
- 引言
- FireBrush
- 分析作品
- 大概流程
- 父類:Particle
- 子類:Fire
- 子類:Smoke
- IceAndFire
- 更多拓展
- Frozen Brush
- 憤怒的小鳥
- 小結
引言
前不久熱劇《權利的游戲》在萬眾期待中大結局了,很多人對它的大結局不滿意,甚至網上有人聯名請愿重拍大結局。但不論如何,這陪伴我們8年的美劇終究還是完結了。
紀念人和事的方式有很多種:文字、音樂、光和影,而代碼也是其中一種。所以,我們可以通過寫代碼去和這部我們追了多年的劇說再見。
大家應該都知道該劇改編自美國作家喬治·R·R·馬丁的奇幻小說《冰與火之歌》系列。正巧大佬Jason Labbe寫了一個和“冰與火”相關的作品,效果如下。
看完大家可能會覺得,你這不是騙我嗎?這明明只有火,哪來的冰呀。確實,這個作品的名字就Fire Brush,和冰毫無關系。但是,大家請稍安勿躁,我在這個作品的基礎上做了一點點修改,完成了Ice and Fire。效果如下,這樣就是名副其實的冰與火之歌了!
Fire Brush這個作品是用P5.js寫的,用粒子系統去模擬了火焰和煙霧的效果。同時作品里面有很多小技巧值得我們去學習。接下來我就給大家介紹一下它的Processing版本,之后介紹如何修改,讓它變成名副其實的"冰與火之歌":凜冬之后,火影搖曳。
??
FireBrush
分析作品
分析Fire Brush作品我們可以獲得如下發現。
該作品由兩種元素構成,火焰和煙霧。拖動鼠標,會增加火焰?;鹧鏁刂髽送蟿拥姆较蛞苿?#xff0c;同時也會向下運動,過一段時間后會消失。在火焰移動的過程中會有煙霧出現,它們會向兩邊跳動,給人一種噴發的感覺,最后也會慢慢消失。
火焰是由一個個圓構成,每個大圓里還有一個小圓,并且它們的透明度不同。每一個煙霧是一個黑色的圓,它們的透明度也不一樣。
還有一個細節就是作品背景的顏色會隨著火焰數量的變化而變化。那么接下來我們就來一起看看如何實現以上的效果。
??
大概流程
我們首先來看大概的流程。我們在setup函數中初始化了我們的粒子系統,然后在draw函數中不斷更新背景的顏色、粒子的狀態,以及監聽按下鼠標左鍵事件,判斷是否需要加入新的火焰。
注意這里顏色的模式不是RGB,而是HSB。并且按下一次鼠標左鍵會調用多次addFire函數,因為按下鼠標不是一瞬間的事情,這段時間會執行多次draw函數。
ParticleSystem ps;void setup() {size(600, 600);colorMode(HSB, 255);ps = new ParticleSystem(); }void draw() {//根據火焰粒子的數量來確定背景顏色的飽和度background(255, ps.getSize(), 255);ps.run();if (mousePressed && mouseButton == LEFT) {//按一次鼠標會執行多次這里的函數ps.addFire(mouseX, mouseY);} }class ParticleSystem {} class Particle {} class Fire extends Particle{} class Smoke extends Particle{}接下來來看ParticleSystem這個類,看它是如何存儲粒子、管理粒子的行為。我們用一個ArrayList來存儲所有的粒子,并且每次更新的時候需要刪除生命走到盡頭的粒子。
class ParticleSystem {ArrayList<Particle> plist;ParticleSystem() {plist = new ArrayList<Particle>();}void run() {for (int i = plist.size() - 1; i >= 0; i--) {Particle p = plist.get(i);p.run();//刪除生命走到盡頭的粒子if (p.isDead()) {plist.remove(i);}}}void addFire(float x, float y) {plist.add(new Fire(x, y));}void addSmoke(float x, float y, float size) {plist.add(new Smoke(x, y, size));}//返回粒子系統的類型是Fire的粒子的數量int getSize() {int cnt = 0;for(Particle p: plist){//如果這粒子是屬于Fire類型的,它的type為1,否者為0cnt += p.type;}return cnt;} }??
父類:Particle
我們現在來看看父類Particle。每一個粒子有一些變量來描述它的運動情況,一些變量來描述它的生命周期,還有一些來描述它的基本屬性。在update函數中根據之前提到的公式會對其位置不斷進行更新。在display函數中首先讓粒子做轉動,然后再進行平移,接著根據剩余生命的長度來進行放縮。
基本的Particle類默認只做拋體運動,也就是加速度大小和方向都固定的運動。
class Particle {PVector location, velocity, acceleration, origin;float angle, aVelocity, aAcceleration;float lifespan, lifeRate, maxLifespan;int type, hue;Particle(float x, float y) {//默認物體繞著自己中心點旋轉origin = new PVector(x, y);//初始化位置為(0,0)location = new PVector();//一個向下的加速度,模擬重力的效果acceleration = new PVector(0, 0.05);//返回一個任意的方向、大小為1的速度velocity = PVector.random2D();lifespan = maxLifespan = 50;//用于控制生命流逝的快慢lifeRate = random(0.35, 1);hue = 20;type = 1;}//根據粒子在生命周期內移動的距離計算它的速度大小float getSpeed(float s){float t = maxLifespan / lifeRate;return s / t;}void run() {update();display();}void update() {//平移velocity.add(acceleration);location.add(velocity);//轉動aVelocity += aAcceleration;angle += aVelocity;//生命流逝lifespan -= lifeRate;}boolean isDead() {if (lifespan < 0.0) {return true;} else {return false;}}void display() {pushMatrix();translate(origin.x, origin.y);rotate(radians(angle));translate(location.x, location.y);scale(map(lifespan, 0, maxLifespan, 0, 1));drawShape();popMatrix();}void drawShape() {stroke(hue, 255, 255);strokeWeight(30);point(0, 0);} }現在大家如果把PartcleSystem里面的addFire函數修改如下的話,可以得到如下圖的效果:
void addFire(float x, float y) {plist.add(new Particle(x, y)); }
??
子類:Fire
一個簡單的粒子系統其實已經完成了,我們現在就需要在這個基礎上進行完善,以達到火焰的效果。
Fire類和它的父類主要有以下幾個不同:
- 由三個粒子構成:Fire粒子的包含三個在其附近的粒子,擁有不同的位置和色調。
- 初速度不同:多了一個從上一幀鼠標位置指向當前鼠標位置的速度,用來達到火焰跟隨鼠標移動的效果。
- update函數不同:在每一次update的時候,會有概率向粒子系統添加煙霧粒子。
- drawShape函數不同:繪制每一個粒子時會繪制兩個大小不同、透明度不同的圓形。
- 多了一個spwan函數:用來向粒子系統添加煙霧粒子。
知道了Fire類和其父類的不同之處,我們就能很容易理解以下的代碼了。
class Fire extends Particle {//存儲包含的粒子ArrayList<PVector> plist;Fire(float x, float y) {super(x, y);//設置初速度PVector v = new PVector(mouseX - pmouseX, mouseY - pmouseY);v.mult(0.1);velocity.mult(getSpeed(100)).add(v);//初始化需要繪制的三個粒子plist = new ArrayList();for (int i = 0; i < 3; i++) {float xOffset = random(-10, 10);float yOffset = random(-10, 10);float hue = random(50);//向量的每個維度分別表示該粒子的:x坐標、y坐標、色調plist.add(new PVector(xOffset, yOffset, hue));}}void update() {super.update();//添加煙霧粒子if (int(random(5)) == 0) {int spawnCount = int(random(3))+1;for (int i = 0; i < spawnCount; i++) {spawn();}}}void spawn() {//根據當前火焰粒子的生命長度來確定煙霧粒子的大小float size = random(25, 50)*map(lifespan, maxLifespan, 0, 1, 0);if (size > 0) {//添加一個煙霧粒子ps.addSmoke(location.x + origin.x , location.y + origin.y, size);}}void drawShape() {for (PVector p : plist) {//繪制大圓stroke(p.z, 255, 255, 20);strokeWeight(80);point(p.x, p.y);//繪制小圓stroke(p.z, 255, 255, 100);strokeWeight(30);point(p.x, p.y);}} }??
子類:Smoke
了解完火焰,我們再來看看煙霧。煙霧和它父類的主要區別如下:
- 運動形式多了轉動:該轉動是勻速轉動,角加速度為0。
- 平移是勻速直線運動:加速度為0。
- 生命周期不一樣:生命周期要短一點。
- drawShape函數不同:每一個圓的顏色為黑色,且其透明度是隨機的。
同樣根據以上不同點,理解下面的代碼也不是難事。這樣,Fire Brush我們就完成了。
class Smoke extends Particle {float size, alpha;Smoke(float x, float y, float _size) {super(x, y);size = _size;alpha = random(10, 150);lifespan = maxLifespan = 30;lifeRate = random(0.4, 1.25);//初始化轉動的角度和初速度angle = random(-45, 45);aVelocity = random(-2, 2);//設置初速的和角速度velocity.set(0, getSpeed(random(-100, -100)));acceleration.mult(0);type = 0;}void drawShape() {stroke(0, 0, 0, alpha);strokeWeight(size);point(0, 0);} }??
IceAndFire
我在FireBrush上做的改動非常的簡單。首先我修改了顏色各個通道的取值范圍,以達到之后粒子顏色漸變更柔和的效果。
void setup() {//..colorMode(HSB, 360);//.. }void draw() {background(300, ps.getSize(), 360);//.. }接下來我對Fire類改做如下的修改。讓每一個粒子的色調不是固定的,是其隨著該粒子剩余的生命長度改變。
class Fire extends Particle {//..Fire(float x, float y) {//..hue = 165;}//..void drawShape() {for (PVector p : plist) {stroke(hue + lifespan * 1.5, 360, 360, 50);strokeWeight(80);point(p.x, p.y);stroke(hue + lifespan * 1.5, 360, 360, 180);strokeWeight(30);point(p.x, p.y);}} }最后我修改Smoke類。除了修改顏色,還將每個粒子從圓形變成了三角形。并且在繪制的時候對其旋轉的一個角度。
class Smoke extends Particle {//..float theta;Smoke(float x, float y, float _size) {//..theta = random(TAU);hue = 10;}void drawShape() {rotate(theta);fill(hue + lifespan * 1.2, 360, 360, alpha);noStroke();triangle(-size / 2, 0, size / 2, 0, 0, size * sqrt(3) / 2);} }??
更多拓展
其實到了現在大家應該發現,借助于繼承和多態,我們很容易對粒子系統進行拓展。我們只用修改它們的運動狀態,渲染方式(繪制的圖形、顏色等),就能獲得一個新的作品。
??
Frozen Brush
大家如果有興趣看以去看看Fire Brush的作者的另一個作品Frozen Brush,這個作品效果非常的驚艷。
但其實原理也很簡單,它也是一個粒子系統。該粒子系統每個粒子的運動方式很基礎,就是隨機一個方向做勻速直線運動,但是它的渲染的方式卻很有意思。該作品不是一個個粒子的渲染,而是選擇其中三個粒子,以這三個粒子為頂點繪制一個三角形。而選擇哪三個粒子作者使用的Delaunay三角剖分算法,簡單的來說,就是把平面上一些點連成如下的效果:
如果大家感興趣的話,我有機會可以給大家詳細介紹以一下這個作品。
??
憤怒的小鳥
在看完這篇文章后,大家完全可以自己實現“憤怒的小鳥”這個前幾年很火的游戲。
該游戲中主要就兩種粒子,鳥和柱子。鳥只是做簡單的自由落體運動,只不過在玩家按下向上鍵的時候,小鳥會獲得一個向上的速度。柱子做的是勻速直線運動,方向向左。唯一有點難度的地方是如何做碰撞檢測,就是如何判斷鳥和柱子相撞了。
我之前用Python寫過這個游戲,給小鳥增加了射擊功能,讓游戲變得簡單了一點,效果如下。
??
小結
又到了和大家說再見的時候了,想必大家一定被粒子系統的魅力所折服了吧。不過,不得不說一句,當粒子系統遇見“繼承和多態”,那簡直是如虎添翼。
看完了冰與火之歌,大家還等什么?是時候用粒子系統去譜寫屬于自己的華麗“樂章”了!
總結
以上是生活随笔為你收集整理的Processing 案例 | 用粒子系统谱写冰与火之歌的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FastJson1.2.24反序列化导致
- 下一篇: 四大天王 -- ContentProvi