請大家想象遊戲中的這樣一個場景:一枚火箭拖着尾煙劃過天際,突然火箭爆炸了,碎片四處飛濺。其中,一具生物的屍體向你飛來,它的碎片飛散開來,帶起蓬蓬血霧,然後在鏡頭上留下混亂的血肉痕迹。這個場景中的所有這些元素的共同點是什麼呢?
是的,多數元素都是混亂的。但從技術的角度講,本場景中的大多數效果得益于一個優秀的粒子系統。煙,火花,血這些現代遊戲中的效果通常使用粒子系統來建立。
為了實作這些效果,你需要構件一個粒子系統,而且不僅僅是一個簡單的系統。你所需要的是一個進階粒子系統,一個快速,便捷,可擴充的系統。如果你是初次接觸粒子系統,我推薦你線閱讀Jeff Lander的文章(The Ocean Spray in Your Face, Graphic Content, July 1998)。Lander的文章和本文的差別在于,前者描述了粒子系統的基本概念,而這裡我着重于如何建構更進階的系統。我将會随本文提供完整的源代碼,你可以下載下傳以驗證本系統。
性能和需要
進階粒子系統可能會需要大量的代碼,是以設計好資料結構是非常重要的。此外必須牢記如果設計欠佳,粒子系統會大幅降低重新整理率,并且大多數的性能問題是由粒子系統帶來的記憶體管理問題引起的。
設計粒子系統時首先應該明白粒子系統大大增加每幀的可見多邊形數量。每個粒子可能需要四個頂點和兩個三角形。以此計算,一個場景中的2,000個可見的雪花粒子将增加4,000個可見的三角形。又因為大多數粒子是運動的,我們不能預先計算頂點緩沖,是以每一幀,定點緩沖都需要被改變。
技巧在于隻執行盡量少的記憶體操作(配置設定和釋放)。這樣,如果一個粒子在一定時間後消亡,不要急着從記憶體中釋放。相反,用一個标志來記錄它是死亡還是重生(重新初始化)。然後當所有的粒子都被标為死亡時,釋放整個粒子系統。(包括系統中的所有粒子),如果系統是定常的,那麼使系統保持存活。如果你想要重建系統或隻想假入一個新的粒子,你應該根據粒子所屬的系統使用相應的預設設定/屬性來自動初始化粒子。
例如,我們在建構一個“煙”系統。當你建立或重建一個粒子時,你可能按表一中的值來設定(當然,血和煙的初始顔色,能量,大小,速度是不同的)。注意各個值亦依賴于系統本身的設定。如果你為煙系統設定風以使煙飄向左方,那麼新粒子的速度必然不同于不受風影響的垂直升起的煙系統。如果你有一個定常的煙系統,且煙粒子的能量是零(如此你就看不見它了),你會想重新初始化它的設定以使它滿載能量地又從底部升起。有的粒子系統可能需要粒子以不同的方式被渲染。例如,你或許會想要幾種血系統,例如噴血,濺血,血池和濺在鏡頭上的血痕,等等。每個效果包含特定的粒子。“噴血”将渲染血在空中噴射的景象,當這些血噴在牆壁上,會調用“濺血”系統,營造出血迹在牆和地闆濺開的效果。血池系統建立出某人被擊斃後地闆上的幾灘血迹。
| |||||||||||||||||||||
表一 |
每個粒子系統都以一種獨特的方式工作。濺血和煙霧的渲染方式是有很大差別的。煙霧粒子通常面對活動鏡頭,而濺血則映射到血濺到的平面上。
當建構粒子系統時,必須考慮到遊戲中所有可能的可變參數,并把他們靈活地建入系統。讓我們再次以煙霧系統為例。我們可能會想改變風的方向向量以使當汽車駛過煙霧時,煙粒子受到汽車帶起的風的影響。
從這點你會意識到每一個系統對于自己的作用而言都是很特殊的。但如果我們想要以某種方式控制系統中還不被系統規則支援的粒子時,該怎麼辦?為了達到這個目的,我們還需要建立一個“手工”粒子系統,以使我們能夠每幀重新整理所有的粒子屬性。
最後我們要考慮的是在引擎層次中連接配接粒子系統的能力。或許我們想要為一支香煙連接配接一個煙霧系統或發光系統。如果角色移動,連接配接到香煙的粒子系統應該得到正确的重新整理。
這就是進階粒子系統的一些基本要求。在下一節,我将講述如何設計一個性能優良的資料結構以實作上述的所有特性。
建立資料結構
現在我們知曉我們需要什麼樣的特性。圖一顯示了将要建立的系統的概貌。請注意在圖中有一個粒子管理器(particle manager),稍候我将對此做出解釋。
在此我們采用從下到上的步驟來設計類,首先是粒子類(particle class)。
粒子類(particle class)。如果你曾經建過粒子系統,你會知道粒子必需的屬性。表一列出了其中一些常見的屬性。
注意粒子的先前位置也可能有用。例如,你可能會想在一個粒子的先前和目前位置之間畫它。火花就是一個從中受益的好例子。你可以在圖2中看到我創造的火花效果。
色彩和能量屬性也能夠用來建立一些有趣的特效。在前一個粒子系統中,我在煙系統中使用色彩,這樣我能夠在場景中動态地為粒子進行光照渲染。
能量值也是非常重要的。能量是與粒子的生命周期相關的-你可以利用這一點來判斷一個粒子是否消亡。而且由于某些粒子的色彩和強度随時間變化(例如火花),你或許會想把它與定點色彩的alpha通道值聯系起來。
我強烈推薦把粒子類的構造函數設為空,因為你并不想在構造的時候使用預設值,這是由于對大多數的例子系統來說,這些值總是不同的。
粒子系統類(the particle system class)這個類是整個系統的核心。重新整理粒子的屬性和設定粒子的形狀都在這個類中進行。我現有的粒子系統類使用我的3D引擎的節點類,其中包含例如位置向量、旋轉四元數和比例值的資料。由于我繼承了這個節點類的所有成員,我能夠在引擎的層次結構中連接配接我的粒子系統,使得引擎能夠改變粒子系統的位置,就如同上面讨論過的香煙系統一樣。但如果你的引擎沒有層次支援,或者如果你在建構一個獨立的粒子系統,這就不需要了。表2列出了在粒子系統基類中你所需要的屬性。
下面将講述如何計算經常面對活動鏡頭的粒子的四個位置。首先,把粒子世界空間坐标變換到鏡頭空間(用世界空間的位置坐标乘以活動鏡頭矩陣)使用粒子的大小屬性來計算四個頂點。
我們使用這四個形成形狀的頂點來渲染粒子,雖然一個粒子隻有一個位置:xyz。為了渲染一個粒子(例如一個火花),我們需要建立一個形狀(用4個頂點)。然後在四個頂點之間我們得到兩個三角形。想象一個非伸展的粒子總是面對你前面的鏡頭,就像圖3所描述的一樣。對我們來說,粒子總是面對活動鏡頭的,這意味着我們可以簡單的加減粒子的鏡頭空間坐标的x和y值。換句話說,保留z值不變就好像你在作2D一樣。你可以在清單1中看到一個計算的例子。
函數(the functions)
現在我們知道在粒子系統的基類中需要什麼屬性,我們能夠開始思考需要什麼樣的函數.既然是基類大多數的函數被聲明為虛函數.每種粒子系統都用不同的方式來更新粒子屬性,是以我們需要一個虛拟的更新函數.這個更新函數将完成以下任務: 更新所有粒子的位置和其他屬性; 如果我們不能預先計算綁定盒,則需更新它; 計算存活的粒子數量.如果沒有存活的粒子則傳回false,反之傳回true.傳回值将被用于判定系統是否可被删除. 現在我們的基類已有能力來更新粒子,我們也将要建立可用新的(也可能是舊的)位置來建構的形狀.這個函數,SetupShape,必須是個虛拟函數,因為某些粒子系統會要伸展粒子,而有些系統不會這麼做.你可以在清單1中看到這樣一個函數.
要向給定的系統加入一個粒子或是重新生成它,建立一個做這件事的函數将是十分有益的.同樣,這應該是個虛拟函數,并應該如此定義:virtual void SetParticleDefault(Particle &p);
就像我在前面解釋過的,這個函數初始化給定粒子的屬性值.但如果你想要改變煙的速度或是影響你的煙系統的風向.這是我們接觸下一個主題:粒子系統的構造函數.許多粒子系統需要他們獨特的構造函數,強迫我們在基類中建立一個虛拟構造函數和析構函數.在基類的構造函數中你應該輸入以下資訊:
你一開始想在系統中建構的粒子數量;
粒子系統的位置;
你想在系統中使用的混合模式;
你想在系統中使用的底紋或底紋檔案名;
系統方式(即ID).
在我的引擎中,粒子系統的基類中的構造函數是這樣的:
virtual ParticleSystem(int nr, rcVector3 centerPos, BlendMode blend=Blend_AddAlpha, rcString file name=óEffects/Particles/ green_particleó, ParticleSystemType type=PS_Manual);
那麼應該在哪兒做例如煙系統的風向的各種設定呢?你可以在構造函數中加入根據特定系統的設定值,也可以在每個類中建立一個名為InitInfo的結構(structure),其中包含所有的必須的設定值.如果你使用後一種方法,確定在構造函數中加入一個新的參數,即指向新結構的指針.如果指針為空(NULL),使用預設的設定值.
正如你所想象的,第一種方法要在構造函數中加入很多參數,而這對于程式員來說過于繁瑣.這是我不使用第一種方法的主要原因.使用第二種方法則簡單得多,我們可以在每一個粒子系統類中建立一個函數以用預設值來初始化它的結構.這段代碼的例子和示範程式可在Game Developer網站(http://www.gdmag.com)或我的網站(http://www.mysticgd.com)上找到.
粒子管理器(the Particle Manager)
現在我們已經發現了每個粒子系統背後的隐藏的技術,該是建立一個管理類來控制我們的各種粒子系統時候 .一個管理類将掌管建立,釋放,更新和渲染所有的系統.這樣,管理類必須有一個屬性是指向粒子系統的指針.我強烈推薦你建立或是使用一個數組模版(array template),因為這将會簡單些.
你制作的粒子系統的使用者或許會希望較容易地增加系統.他們也不想跟蹤所有的系統以保證所有粒子均已死亡進而可以從記憶體中釋放它們.這就是設計管理類的目.管理器将會在需要時自動更新和渲染系統,并删除已死亡的系統.
當使用不定時系統(系統将在給定的時間後死亡)時,有一個檢查系統是否已被删除的函數是很有用的(例如,是否它還存在于粒子管理器中).想象你建立了一個系統并存儲了指向系統的指針.你通過這個指針每一幀通路系統.如果系統恰巧在你使用指針之前死亡,會發生什麼?崩潰.這就是為什麼我們需要一個檢查系統是否存活還是已經被管理類删除的函數.粒子管理類中需要的函數清單如表3所示.
AddSystem 函數可能隻有一個參數:指向粒子類的指針.這将允許你根據需要輕易地增加一個煙系統或火系統.下面是我如何在引擎中增加一個粒子系統的例子: gParticleMgr->AddSystem( new Smoke(nrSmokeParticles, position, ...) ); 在世界更新函數中我調用了gParticleMgr->Update()函數,這個函數自動更新所有的系統并釋放死亡的系統.Render函數然後渲染所有的可見粒子系統.
因為我們不想在每一幀跟蹤系統中的所有粒子以判斷是否所有的粒子均已死亡(這樣系統可被删除),我們将使用Update函數來代替.如果這個函數傳回TRUE,這意味着系統處于存活狀态;反之系統已死亡并可以被删除.粒子管理程式的Update函數如清單2所示.
在我的粒子系統中,其中配置設定的所有具有相同底紋和混合方式的粒子将連續地渲染,同時将底紋轉換和上載減到最小.這樣,如果在螢幕上有十個可見的煙系統,就隻有一個底紋轉換和狀态改變将被執行.
設計,然後再編碼
設計一個靈活的,快速的和可擴充的進階粒子系統并不很困難,如果你花時間來考慮你将如何在遊戲中使用它,并且由此細緻設計你的系統.由于在此讨論的系統使用帶有繼承的類,你也可以在.DLL檔案中加入獨立的粒子系統形式.這就使建立某種plug-in系統成為可能,而這可能是某些開發者所感興趣的. 你也可以下載下傳我的粒子系統(我為Oxygen3D開發的最新的引擎)的源程式.這些源碼不是獨立的可編輯系統,但如果你遇到問題,它應該能幫助你.如果你還有任何問題或意見,請直接給我發電子郵件.
以下是清單或清單
TA B L E 2 . Particle system base class attributes. | ||
Data type | Name | Description |
Texture | *texture | A pointer to a texture, which all particles will use. For performance reasons, we only use one texture for each individual particle system; all particles within the specific system will have the same texture assigned. |
BlendMode | blendMode | The blend mode you want to use for the particles. Smoke will probably have a different blend mode from blood-that's the reason you also store the blend mode for each particle system. |
int | systemType | A unique ID, which represents the type of system (smoke or sparks, for example). The systemType identifier is also required, since you may want to check for a specific type of particle system within the collection of all systems. For example, to remove all smoke systems, you need to know whether a given system is a smoke system or not. |
Array Particle | particles | The collection of particles within this system. This may also be a linked list instead of an array. |
Array PShape | shapes | A collection of shapes, describing the shapes of the particles. The shape descriptions of the particles usually consist of four positions in 3D camera-space. These four positions are used to draw the two triangles for our particle. As you can see in Table 1, a particle is only stored as a single position, but it requires four positions (vertices) to draw the texture-mapped shape of the particle. |
int | nrAlive | Number of particles in the system which are still alive. If this value is zero, it means all particles are dead and the system can be removed. |
BoundingBox3 | boundingBox | The 3D axis-aligned bounding box (AABB), used for visibility determination. We can use this for frustum, portal, and anti-portal checks. |
TA B L E 3. Particle manager class functions. | |
Init | Initializes the particle manager. |
AddSystem | Adds a specified particle system to the manager. |
RemoveSystem | Removes a specified particle system. |
Update | Updates all active particle systems and removes all systems which died after the update. |
Render | Renders all active and visible systems. |
Shutdown | Shuts down the manager (removes all allocated systems). |
DoesExist | Checks whether a given particle system still exists in the particle manager (if it has not been removed yet). |