第六章:粒子特效
絢麗的火焰與爆炸
本章節主要介紹粒子特效設計的方法論,其中有相當的知識量是平臺無關的;在本文中會以“爆炸”這個實際的例子爲線索,進行詳細的設計講解,並最終使用GEiv實現它。
[爲什麼要使用”粒子”]
實現粒子特效的首要目的,是對一些環境效果進行模擬仿真,常見的環境效果,例如火焰、爆炸、雨、雪、霧等,都是無數微小的粒子以某些規律共同作用的結果。而對於計算機來講,雖然沒有足夠的運算能力對每一個自然粒子進行抽象,但我們可以借鑑其原理,使用相對更少的粒子對這些自然現象進行模擬和仿真,以達到近似的效果。
[需要設計哪些內容]
[粒子屬性]
首先需要設計的是單個粒子的屬性,這裏我們以粒子個體作爲考慮的焦點,考慮的內容往往是粒子的共有屬性,屬性的內容可以是圖形樣式、大小、顏色等等。
[投射規律]
投射規律考慮粒子以何種方式投射到屏幕上,這裏以粒子羣爲考慮的焦點,考慮的內容會涉及到實際的物理規律,例如粒子在空間中的角度分佈、速度分佈以及顆粒大小分佈等情況。
[演變規律]
演變規律是拋射後的粒子隨着時間變化的規律,它同樣會涉及到物理規律的模擬,只不過這次是針對單個粒子的設計,例如速度、自旋角度、顏色等屬性的變化規律。
[實例-模擬爆炸]
爆炸特效在遊戲中的使用相當廣泛,屬於經典的粒子系統。現在我們從零開始,設計一個爆炸的粒子特效。
在想要模擬爆炸前先來觀察一個實際的爆炸例子:
從圖片中我們能夠概況一些基本的物理規律:
首先,在一個爆炸中,粒子的大小顯然是不同的,而且,簡單的想,粒子的大小與其質量成正比,所以粒子速度應該與其大小負相關,你可以看到顆粒狀的小型碎片已經飛到了火焰之外的區域,這是動量守恆定律所確定的。
其次,在爆炸的中心,能量較高,呈現出亮白色;而在爆炸的外圍,與空氣接觸後熱量明顯下降,火焰呈現出暗紅色,在這個過程中,顏色也呈現出了明顯的變化規律:亮白-》黃色-》紅色-》暗紅。
還有,速度的變化規律:在爆炸發生後,粒子的速度並不會一直不變,它還要受到空氣阻力的作用,根據流體力學的相關內容,空氣阻力與速度的平方成正比,與物體在運動方向的正投影面積成正比,所以其速度變化應該表現爲某種受到阻尼的運動狀態。
最後,在能量耗盡的暗紅色區域,粒子逐漸消失,也就是說其顏色通道係數應該以某種非線性(先慢後快)的方式衰減。
[屬性設計]
粒子圖元:首先需要確定的問題,我們如何選擇粒子的圖形呢,使用點?圓形?方塊?還是使用某種貼圖呢……其實設計粒子的基本形態很值得一說,我們暫且使用圓形來設計,在最後您可以看到更改粒子形態對整體特效的影響。
粒子的顏色:由白到紅,初始值使用白色。
粒子的大小:爲了較爲明確的產生大小兩種粒子,我將使用一定的概率分佈策略隨機產生大小(詳見投射設計部分)。
自旋角度:在圓周上均勻分佈,由於一開始我們使用圓形作爲圖元,所以這個自旋這個屬性不會顯露出來。
通道:Alph初始值設置爲1.0。
[投射設計]
產生:在我們給定爆炸點之後,假定粒子圍繞着給定點進行+/-5位置浮動的隨機的產生。
大小分佈:以50%的概率產生6~35大小的粒子,否則產生6~24大小的粒子,這裏只是一個簡單方案,你也可以考慮使用高斯分佈等。
速度方向分佈:以產生點進行360度均勻分佈。
速度大小分佈:爲了簡化選擇了恆定值,但是,空氣阻力模型在演變中起到作用,故仍可觀察到非常近似地模擬結果。
[演變設計]
速度衰減:
對於每一幀:v -= a*w^2*v;其中,w是粒子大小,a是衰減係數,v是當前速度,也就是說,速度進行阻尼衰減,並且大碎片的速度衰減的更快。
顏色衰減:
↑衰減時間圖
↑衰減過程均勻抽樣
首先,RGB中的紅色分量是不變的。
假如把時間t變量規格化到0~1之間。
那麼,藍色分量應該最快衰減,因爲爆炸主色調至少應該是一個暖色調。所以藍色線使用的是t^16。
綠色分量暫時設置爲伴隨t的線性衰減,其實,G分量衰減速度可以依據大小而定以獲得更逼真的效果。
通道衰減:
↑衰減時間圖
↑衰減過程均勻抽樣
通道衰減過程先慢後快,這樣,在特效開始的一段時間內,我們不會感到通道的變化,直到粒子快要消亡時纔會有直觀的視覺感受。
[編碼實現]
接下來就是編碼階段了,我們也明確的看到,其實整個粒子特效的實現過程中,設計佔了相當大的比例,在最後的階段,只不過是要我們使用擅長的平臺去實現罷了,其實很多軟件開發都是這樣的,編碼只是個實現過程,不是什麼高科技。
您可以到GitHub上找到本章中的例子。這裏進入
在Geiv下,我們的粒子僅需要實現Individual接口,並使用個體的集羣管理器進行管理即可(參閱第五章)。
//ExpIndividual.java:
package com.geiv.test;
import engineextend.crowdcontroller.Individual;
import geivcore.UESI;
import geivcore.enginedata.obj.Obj;
import java.awt.Color;
import com.thrblock.util.REPR;
import com.thrblock.util.RandomSet;
public class ExpIndividual implements Individual {
UESI UES;
Obj disp;
float sTallms = 500;//這裏設置了粒子從產生到消亡的總經歷時間
float allms = sTallms;
float Dms = 17;//這裏設置了每一幀的時間,你也可以用1000/UES.getFPS這中方法在構造器裏填充
float V = 4.5f;//運動的初始速度被固定爲4.5像素每幀
float ax, ay;
float vx, vy;
float Theta;//自選角度,本例中暫時使用圓形,所以是看不出的
public ExpIndividual(UESI UES) {
this.UES = UES;
disp = UES.creatObj(UESI.XRIndex);//這裏把圖元產生在了XR層,前面的章節中介紹了該層次混合模式的特點。
disp.addGLOval("FFFFFF",0,0,12,12,12);//畫一個圓形
disp.setGLFill(true);
disp.setColor("FFFFFF");
disp.setAlph(disp.getTopDivIndex(), 1.0f);
allms = sTallms;
}
@Override
public boolean isAvalible() {
return !disp.isPrintable();//關於Individual請參考第五章的介紹
}
@Override
public void getUse(Object[] ARGS, float... FARGS) {
int Rad;//我們使用一定的分佈方法產生Rad大小,RandomSet是內置的隨機數發生器,其靜態方法名稱都比較好理解,就不在這裏細細講解了。
if (RandomSet.getRate(50)) {//以50%的概率返回布爾值true
Rad = RandomSet.getRandomNum(6, 35);//返回6~35隨機數,均勻分佈。
} else {
Rad = RandomSet.getRandomNum(6, 24);
}
disp.setWidth(Rad);
disp.setHeight(Rad);
//初始位置具有+/-5的浮動區域
disp.setCentralX(FARGS[0] + RandomSet.getRandomNum(-5, 5));
disp.setCentralY(FARGS[1] + RandomSet.getRandomNum(-5, 5));
//初始自選角度,0~360均勻分佈。
disp.setAngle(RandomSet.getRandomFloatIn_1() * 360);
//速度角,0~2PI均勻分佈,使用弧度是爲了方便調用Math下的三角函數。
Theta = (float) Math.PI * 2 * RandomSet.getRandomFloatIn_1();
vx = V * (float) Math.sin(Theta);//計算橫縱向速度
vy = -V * (float) Math.cos(Theta);
ax = -0.0003f * (disp.getWidth() * disp.getWidth()) * vx;//計算加速度
ay = -0.0003f * (disp.getHeight() * disp.getHeight()) * vy;
disp.show();//顯示到屏幕上(投射完成)
}
@Override
public void doStp(int clock) {
if (this.allms > Dms) {
allms -= Dms;//allms記錄當前剩餘存活期,使用這個變量是爲了將存活期規格化到0~1之間。
//REPR是內置的變換工具,可以將一個規格化後的線性量轉化爲自定義的常用非線性量
//顏色變化
disp.setColor(new Color(1.0f, REPR.Rep_POW_1_F(allms / sTallms, disp.getWidth() / 24), REPR.Rep_POW_F(allms / sTallms, 16)));
//通道變化
disp.setAlph(REPR.Rep_POW_1_F(allms / sTallms, disp.getWidth() / 12));
//運算加速度
ax = -0.0003f * (disp.getWidth() * disp.getWidth()) * vx;
ay = -0.0003f * (disp.getHeight() * disp.getHeight()) * vy;
//運算速度
vx += ax;
vy += ay;
//運算位置
disp.setDx(disp.getDx() + vx);
disp.setDy(disp.getDy() + vy);
} else {
//生命週期結束後,將粒子資源回收
finish(Individual.SRC_INNER);
}
}
@Override
public void finish(int src) {
disp.hide();
//重置顏色與通道
disp.setColor("FFFFFF");
disp.setAlph(disp.getTopDivIndex(), 1.0f);
//重置大小
disp.setWidth(12);
disp.setHeight(12);
//重置存活時間。
allms = sTallms;
}
@Override
public void destroy() {
disp.destroy();
}
}
//Explosion.java:
package com.geiv.test;
import engineextend.crowdcontroller.CrowdController;
import geivcore.UESI;
public class Explosion{
UESI UES;
CrowdController cc;
public Explosion(UESI UES)
{
this.UES = UES;
cc = new CrowdController(UES, true);
for(int i = 0;i < 512;i++)//裝入了512個粒子資源
{
cc.addIndividual(new ExpIndividual(UES));
}
}
public void doEffect(float dx,float dy) {
for(int i = 0;i < 128;i++)//當每次調用時,分配128個粒子資源,同時也意味着,您可以同時在屏幕上產生4個異步的爆炸特效。
{
cc.getAvailible().getUse(null,dx,dy);
}
}
public void forceClose() {
cc.finishAllInd();
}
}
//Main.java:
package com.geiv.test;
import geivcore.R;
import geivcore.UESI;
public class Main{
public static void main(String[] args) {
UESI UES = new R();
Explosion exp = new Explosion(UES);
for(;;){
exp.doEffect(400,300); //產生一個爆炸
UES.wait(3,1000); //延時1秒
}
}
}
執行效果:
[粒子特效的改進]
“一堆圓形一點兒也不像嘛”這是我同學看到程序後的第一句評價,的確,從粒子的行爲模式上來講,是有類似爆炸的性質了,不過一個爆炸也不能只讓圓形組成不是嗎?
↑給一個大點的圖,可以更突出的發現這個問題
在屬性設計時,我提到了關於粒子圖元選擇的問題,對於爆炸這個特效,顯然均勻的圓形(或者其他圖形)不是一種好的圖元構成,我們需要一個形狀並不均勻,甚至伴有隨機性的圖形來替換這個圓,於是筆者想到了“雲”這個東西。
↑由於雲是白色的,所以爲了展示,把PS的襯底一起截下來了。
雲本來是與爆炸毫不相干的東西,選擇它是由它的圖形性質決定的:邊緣漸變、具有隨機性、在顏色通道上也不均勻。而且,加上我們之前定義的自旋隨機分佈,加入自旋角的雲看起來和彼此具有更大的差異。
爲了使用雲這個素材,先把它放在項目目錄裏:
之後找到ExpIndividual類,找到它的圖元繪製部分:
我們把
disp.addGLOval("FFFFFF",0,0,12,12,12);
disp.setGLFill(true);
這兩行改爲:
disp.addGLImage(0, 0, 12, 12,".\\Effect\\PT_CLOUD1_POINT.png");
經過改進的特效:
↑可以跟圓形做一下對比,是不是好多了呢
[總結]
本章介紹了粒子特效設計的基本步驟,即屬性、投射、演化三部分。
粒子特效是對自然的模擬,因此在設計時要充分地考慮到物理因素,這樣會得到更好的仿真結果。
最後,恰當地選擇粒子圖元可以得到更好的結果,而粒子圖元的選擇與圖形的性質有關。