1. 程式人生 > >【一步步學OpenGL 28】 -《Transform Feedback粒子系統》

【一步步學OpenGL 28】 -《Transform Feedback粒子系統》

教程 28

Transform Feedback粒子系統

http://ogldev.atspace.co.uk/

背景

粒子系統是一種模擬像煙霧、灰塵、火焰、雨等自然現象的技術統稱。這些自然現象的共性是它們都是由大量的小粒子組成的,並按照不同現象的特性進行特定形態的整體運動。

為了模擬由大量粒子組成的自然現象,我們通常要維護每個粒子的位置資訊和其他一些屬性(例如速度、顏色等等),並且在每一幀要進行下面的一些操作:

  1. 更新每一個粒子的屬性。這一步通常包含一些數學計算(從簡單計算到複雜計算,複雜性取決於要模擬的自然現象的複雜度)。

  2. 渲染粒子(將每個點渲染成簡單的顏色或者利用billboard公告板技術實現)

在過去,步驟一通常發生在CPU上,應用將會訪問頂點緩衝器,掃描裡面的內容並且更新每個粒子的屬性。步驟二更直接,和其他型別的渲染一樣發生在GPU上。但這種方法存在兩個問題:

  • 在CPU上更新粒子需要OpenGL驅動從GPU記憶體中複製頂點緩衝器中的內容到CPU中(在獨立顯示卡中這意味著要通過PCI匯流排傳輸資料)。通常我們需要模擬的現象是需要大量的粒子的,數以萬計的粒子需求情況也不是少數。如果每個粒子都要佔用64 bytes且執行時每秒60幀(很好的幀率了),意味著每秒鐘要在GPU和CPU之間來回拷貝640K的內容60次,這會大大影響應用的效能,且隨著粒子數量的增加效能也會越差。

  • 更新粒子屬性意味著在不同的資料項中執行相同的數學運算,這是一個GPU最擅長的典型的分散式計算的例子,而如果執行在CPU上則意味著要序列執行整個過程。如果CPU是多核的,那麼我們可以利用它降低整體的計算時間,但這會需要在應用中做更多額外的工作。而將更新過程執行在GPU上就相當於可以直接獲得並行執行的效果。

DirectX10引入了一種叫做Stream Output的新特性,對於實現粒子系統很有用。OpenGL3.0中最後也加入了該新特性並稱之為Transform Feedback。該特性背後的思想是,我們可以繫結一個特殊型別的緩衝器(稱其為Transform Feedback Buffer,就在幾何著色器之後,當然如果幾何著色器被忽略的話就是在頂點著色器之後了),並將變換之後的圖元傳遞給該緩衝器。另外,我們可以決定是否讓圖元繼續進行後面常規的光柵化流程。相同的緩衝器下,上一幀輸出的頂點資訊可以作為快取用於下一幀的輸入。在這個迴圈中,上面的兩步可以完全發生在GPU上而不需要我們應用的參與(並不是為每一幀都繫結相應的緩衝器並設定一些狀態)。下面的示意圖展示了管線的新的結構:

那麼在transform feedback buffer中最後到底有多少圖元呢?如果沒有用到幾何著色器答案就簡單了,只要根據當前幀的頂點數即可直接計算。但是如果用到了幾何著色器,那麼圖元的數量就是未知的了,因為在幾何著色器過程中是可以新增或者刪除圖元的(甚至可以包含迴圈和分支),我們無法總能在緩衝器中計算最終的圖元數量。因此,不知道確切的頂點數量之後我們怎麼繪製呢?為了解決這個困難,transform feedback引入了一種新的繪製函式,這個函式回撥不需要使用頂點數量作為引數。系統會自動為每一個buffer計算頂點數量,之後當buffer快取作為輸入時可在內部使用那個計算好的頂點數量。如果多次將資料輸入到transform feedback buffer中(緩衝到裡面但是不作為之後的輸入)相應的頂點數量也會隨之更新增加。我們可以選擇隨時在buffer內部更新快取偏移值,同時系統也會根據我們設定的偏移值更新頂點數量值。

在這個教程中,我們將使用transform feedback來模擬火焰效果。火焰模擬中的數學計算相對來說比較容易,因此這裡我們重點介紹transform feedback的使用和實現。該框架之後也可以應用到其他型別的粒子系統。

OpenGL有一個強制限制,就是在同一個繪製回撥中相同的資源不可以同時繫結作為輸入和輸出。這意味著如果我們想在頂點緩衝器中更新粒子,我們實際需要兩個transform feedback buffer並交替使用它們。在第0幀,我們將在buffer A中更新粒子,並在buffer B中渲染粒子。然後在第1幀中我們將在buffer B中更新粒子,在buffer A中渲染粒子。當然所有這些使用者是無需關心的。

此外,我們也有兩個著色器:一個負責更新粒子,另一個負責渲染粒子。我們也會使用前面教程中介紹的公告板技術來渲染。

原始碼詳解

(particle_system.h:29)

class ParticleSystem
{
public:
    ParticleSystem();

    ~ParticleSystem();

    bool InitParticleSystem(const Vector3f& Pos);

    void Render(int DeltaTimeMillis, const Matrix4f& VP, const Vector3f& CameraPos);

private:

    bool m_isFirst;
    unsigned int m_currVB;
    unsigned int m_currTFB;
    GLuint m_particleBuffer[2];
    GLuint m_transformFeedback[2];
    PSUpdateTechnique m_updateTechnique;
    BillboardTechnique m_billboardTechnique;
    RandomTexture m_randomTexture;
    Texture* m_pTexture;
    int m_time;
};

ParticleSystem類封裝了所有管理transform feedback buffer的機制,應用可以例項化ParticleSystem類並使用火焰發射器的世界座標位置初始化它。在主渲染迴圈中,ParticleSystem::Render()函式會被呼叫,並接收三個引數:從上一個回撥的毫秒時間差,檢視視窗矩陣和投影矩陣的乘積,和相機的世界空間位置。

這個類還有幾個屬性:一個是Render()函式第一次被呼叫的標記變數;兩個索引,一個指明當前的定點緩衝器(作為輸入),另一個指定transform feedback buffer(作為輸出);還有兩個頂點緩衝器控制代碼和兩個transform feedback物件控制代碼;更新和渲染著色器;一個包含隨機數的紋理,這個紋理將會貼到離子上;最後還有當前的全域性時間變數。

(particle_system.cpp:31)

struct Particle
{
    float Type; 
    Vector3f Pos;
    Vector3f Vel; 
    float LifetimeMillis; 
};

每一個粒子都可用上面的結構體來表示。一個例子可以是一個發射器,可以是發射器分裂產生的shell或者secondary shell。發射器是靜態的,負責產生其他粒子,且在系統中是獨一無二的。發射器週期性的建立shell粒子,並往上發射出去,幾秒後shells爆炸分裂出secondary shells並飛向隨機方向。除了發射器,所有的粒子都有它們的生命週期,系統以毫秒為時間單位計算它們。當粒子的生命週期達到一定時間後就會被移除。另外每個粒子都有它們的位置和速度。當一個粒子被建立後會被給予一個速度向量,這個速度會受到重力的影響使其下落。在每一幀,我們使用速度向量來更新粒子的世界座標,然後使用更新的座標來渲染粒子。

(particle_system.cpp:67)

bool ParticleSystem::InitParticleSystem(const Vector3f& Pos)
{ 
    Particle Particles[MAX_PARTICLES];
    ZERO_MEM(Particles);

    Particles[0].Type = PARTICLE_TYPE_LAUNCHER;
    Particles[0].Pos = Pos;
    Particles[0].Vel = Vector3f(0.0f, 0.0001f, 0.0f);
    Particles[0].LifetimeMillis = 0.0f;

    glGenTransformFeedbacks(2, m_transformFeedback); 
    glGenBuffers(2, m_particleBuffer);

    for (unsigned int i = 0; i < 2 ; i++) {
        glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_transformFeedback[i]);
        glBindBuffer(GL_ARRAY_BUFFER, m_particleBuffer[i]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(Particles), Particles, GL_DYNAMIC_DRAW);
        glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_particleBuffer[i]);
    }

這是粒子系統初始化的第一部分。我們在棧上為所有的粒子開闢空間,並只初始化第一個粒子作為發射器(其他的粒子在渲染時再建立)。發射器的位置也是所有要建立的粒子的起點,發射器的速度也是所有新建立粒子的初始速度(發射器自己是靜止的)。我們將要使用兩個transform feedback緩衝器並在他們之間切換(使用其中一個繪製輸出的同時,使用另一個作為輸入,反之亦然)。我們可以使用glGenTransformFeedbacks函式建立兩個transform feedback物件,它們封裝了所有繫結到它們上面的狀態。另外建立兩個緩衝器物件,分別用於兩個transform feedback物件,對於這兩個物件我們將進行一系列相同的操作(見下文)。

開始我們先使用glBindTransformFeedback()函式將一個transform feedback物件繫結到GL_TRANSFORM_FEEDBACK目標上,這樣該transform feedback物件就變成當前物件,下面和transform feedback相關的操作就都是針對當前物件的。然後將對應的緩衝器物件繫結到GL_ARRAY_BUFFER上,使其成為一個常規的頂點緩衝器,並載入粒子陣列的資料內容到緩衝器裡面。最後我們繫結相應的緩衝器物件到GL_TRANSFORM_FEEDBACK_BUFFER目標上面,並定義緩衝器索引值為0,使該緩衝器成為一個索引位置為0的transform feedback緩衝器。事實上我們可以將多個緩衝器繫結到不同的索引位置上,這樣圖元可以輸入到不同的緩衝器中,這裡我們只需要一個緩衝器。現在我們就有了兩個transform feedback物件和對應的兩個緩衝器物件,兩個緩衝器物件既可以作為頂點緩衝器也可以作為transform feedback緩衝器。

InitParticleSystem()函式剩下的部分就不需要再重複解釋了,並沒有什麼新內容,我們只需簡單初始化兩個著色器物件(ParticleSystem類的成員),並設定其中一些靜態狀態,以及載入將要貼到粒子上的紋理。

(particle_system.cpp:124)

void ParticleSystem::Render(int DeltaTimeMillis, const Matrix4f& VP, const Vector3f& CameraPos)
{
    m_time += DeltaTimeMillis;

    UpdateParticles(DeltaTimeMillis);

    RenderParticles(VP, CameraPos);

    m_currVB = m_currTFB;
    m_currTFB = (m_currTFB + 1) & 0x1;
}

這是ParticleSystem類的主渲染函式,負責更新全域性計時器,並在兩個快取之間進行切換(’m_currVB’是當前頂點緩衝器索引初始化為0,而’m_currTFB’是當前transform feedback緩衝器初始化為1)。這個函式的主要作用是呼叫兩個更新粒子屬性的私有方法並進行渲染。下面看如何更新粒子。

(particle_system.cpp:137)

void ParticleSystem::UpdateParticles(int DeltaTimeMillis)
{
    m_updateTechnique.Enable();
    m_updateTechnique.SetTime(m_time);
    m_updateTechnique.SetDeltaTimeMillis(DeltaTimeMillis);

    m_randomTexture.Bind(RANDOM_TEXTURE_UNIT);

開始先啟用相應的著色器並設定其中的一些動態狀態。著色器要知道從上一幀到這一幀的時間間隔,因為在計算粒子的位移公式中需要用到這個引數,另外還需要一個全域性的時間引數作為隨機數種子來訪問隨機紋理。我們宣告一個GL_TEXTURE3紋理單元來繫結隨機紋理。這個隨機紋理是用來為產生的粒子提供運動方向的,而不是提供顏色資訊(後面會介紹紋理是如何建立的)。

    glEnable(GL_RASTERIZER_DISCARD);

這個個函式回撥用到了我們之前沒有接觸過的東西。由於該渲染回撥到該函式的唯一目的就是為了更新transform feedback緩衝器,然後我們想截斷圖元傳遞流,並阻止它們經光柵化後顯示到螢幕上。阻止的這些渲染操作之後將會在另一個渲染回撥中再執行。使用GL_RASTERIZER_DISCARD標誌作為引數呼叫glEnable()函式,告訴渲染管線在transform feedback可選階段之後和到達光柵器前拋棄所有的圖元。

    glBindBuffer(GL_ARRAY_BUFFER, m_particleBuffer[m_currVB]); 
    glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m_transformFeedback[m_currTFB]);

接下來的兩個函式用來切換我們建立的兩個緩衝器。’m_currVB’(01)作為頂點快取陣列的一個索引,並且我們將這個快取繫結到GL_ARRAY_BUFFER上作為輸入。’m_currTFB’(總是和’m_currVB’相反)作為transform feedback物件陣列的一個索引並且我們將其繫結到GL_TRANSFORM_FEEDBACK目標上,使其成為當前的 transform feedback(連同附著在其上的狀態——實際的快取)。

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    glEnableVertexAttribArray(2);
    glEnableVertexAttribArray(3);

    glVertexAttribPointer(0,1,GL_FLOAT,GL_FALSE,sizeof(Particle),0); // type
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(Particle),(const GLvoid*)4); // position
    glVertexAttribPointer(2,3,GL_FLOAT,GL_FALSE,sizeof(Particle),(const GLvoid*)16); // velocity
    glVertexAttribPointer(3,1,GL_FLOAT,GL_FALSE,sizeof(Particle),(const GLvoid*)28); // lifetime

上面這幾個函式我們之前都使用過,都是根據頂點緩衝區中的資料設定頂點屬性。後面會看到我們如何保證輸入的結構和輸出結構保持一致的。

    glBeginTransformFeedback(GL_POINTS);

The real fun starts here. glBeginTransformFeedback() makes transform feedback active. All the draw calls after that, and until glEndTransformFeedback() is called, redirect their output to the transform feedback buffer according to the currently bound transform feedback object. This function also takes a topology parameter. The way transform feedback works is that only complete primitives (i.e. lists) can be written into the buffer. This means that if you draw four vertices in triangle strip topology or six vertices in triangle list topology, you end up with six vertices (two triangles) in the feedback buffer in both cases. The available topologies to this function are therefore:
有趣的部分來了。我們呼叫glBeginTransformFeedback()函式來啟用transform feedback。在這之後的所有繪製的結果(直到glTransformFeedback()被呼叫)都會被輸入到當前的transform feedback快取中(根據當前繫結的transform feedback物件)。這個函式需要一個拓撲結構變數作為引數。由於Transform feedback工作的方式,只有完整的圖元才能被寫入到快取中。這個意思就是如果你繪製四個頂點(其拓撲結構是triangle strip),或者六個頂點(其拓撲結構是triangle list),不論你使用哪種方式最後輸入到這個快取中的資料都將是六個頂點(兩個三角形)。對於這個函式的引數可以是下面這幾個:

GL_POINTS - the draw call topology must also be GL_POINTS.
GL_LINES - the draw call topology must be GL_LINES, GL_LINE_LOOP or GL_LINE_STRIP.
GL_TRIANGLES - the draw call topology must be GL_TRIANGLES, GL_TRIANGLE_STRIP or GL_TRIANGLE_FAN.

    if (m_isFirst) {
        glDrawArrays(GL_POINTS, 0, 1);
        m_isFirst = false;
    }
    else {
        glDrawTransformFeedback(GL_POINTS, m_transformFeedback[m_currVB]);
    } 

如之前所說,我們不知道在快取中有多少個頂點,但transform feedback支援這種情況。因為我們頻繁的生成和刪除粒子是基於發射器和每個粒子的生命週期,我們不可能告訴繪製函式有多少個粒子需要繪製。除了第一次渲染之外都是這樣。在進行第一次渲染時我們知道在頂點緩衝區只包含發射器粒子,並且系統之前沒有transform feedback相關的記錄(第一幀渲染之後我們都沒使用過它),所以它也不知道快取中到底存放了多少粒子。這就是為什麼第一次繪圖一定要明確的使用一個標準的glDrawArrays()來繪製。在其他情況下,我們都會呼叫glDrawTransformFeedback()來完成繪製。這個函式不需要被告知有多少頂點需要渲染,它僅僅檢查輸入快取中的資料並將之前寫入到這個快取中的頂點資料全部渲染出來(只有當它被繫結作為一個transform feedback快取時才行)。

glDrawTransformFeedback()需要兩個引數。第一個引數是繪製的圖元的拓撲結構,第二個引數是當前被繫結到頂點緩衝區上的transform feedback物件。記住當前繫結的transform feedback物件是m_transformFeedback[m_currTFB]。這是繪製函式的目標緩衝區。將要處理的頂點的個數來自於在上一次在這個函式中被繫結到GL_TRANSFORM_FEEDBACK目標上的transform feedback物件。有點混亂,我們只需要簡單的記住當我們向transform feedback物件#1中寫入的時候,就從transform feedback物件#0中得到頂點的個數來進行繪製,反之亦然。現在的輸入將作為以後的輸出。

    glEndTransformFeedback();

每一次呼叫glBeginTransformFeedback()之後一定要記得呼叫glEndTransformFeedback()。如果漏掉這個函式,將會出現很大的問題。

    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);
    glDisableVertexAttribArray(3);
}

這一部分是很常見的,當執行到這裡的時候,所有的粒子都已經被更新過了,接下來讓我們看看如何對更新之後的粒子進行渲染。

(particle_system.cpp:177)

void ParticleSystem::RenderParticles(const Matrix4f& VP, const Vector3f& CameraPos)
{
    m_billboardTechnique.Enable();
    m_billboardTechnique.SetCameraPosition(CameraPos);
    m_billboardTechnique.SetVP(VP);
    m_pTexture->Bind(COLOR_TEXTURE_UNIT);

我們通過啟用billboarding技術對粒子進行渲染,在渲染之前我們為這個著色器設定了一些引數。每個粒子將被擴充套件為一個四邊形平面,這裡繫結的紋理最終會被對映到這個平面上。

    glDisable(GL_RASTERIZER_DISCARD);

在我們將資料寫入到transform feedback快取中時,光柵化被禁用了。我們使用其glDisable()函式來啟用光柵器。

    glBindBuffer(GL_ARRAY_BUFFER, m_particleBuffer[m_currTFB]);

當我們寫資料進transform feedback快取時,我們將m_transformFeedback[m_currTFB]繫結位為transform feedback物件,附著在那個物件上的頂點緩衝區就是m_particleBuffer[m_currTFB]。我們現在繫結這個快取來為渲染提供輸入頂點。

    glEnableVertexAttribArray(0);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Particle), (const GLvoid*)4); // position

    glDrawTransformFeedback(GL_POINTS, m_transformFeedback[m_currTFB]);

    glDisableVertexAttribArray(0);
}

在transform feedback快取中的粒子有四個屬性。但是在渲染粒子的時候,我們只需要知道粒子的位置資訊,所以我們只啟用了位置屬性對應的位置,同時我們確定間隔(相鄰的同屬性之間的距離)為sizeof(Particle),而其他的三個屬性被我們忽略。

同樣,我們還是使用glDrawTransformFeedback()來繪製。第二個引數是transform feedback物件(該物件被匹配到輸入的頂點緩衝區)。這個物件知道有多少個頂點要被繪製。

(ps_update_technique.cpp:151)

bool PSUpdateTechnique::Init()
{
    if (!Technique::Init()) {
        return false;
    }

    if (!AddShader(GL_VERTEX_SHADER, pVS)) {
        return false;
    }

    if (!AddShader(GL_GEOMETRY_SHADER, pGS)) {
        return false;
    }

    const GLchar* Varyings[4]; 
    Varyings[0] = "Type1";
    Varyings[1] = "Position1";
    Varyings[2] = "Velocity1"; 
    Varyings[3] = "Age1";

    glTransformFeedbackVaryings(m_shaderProg, 4, Varyings, GL_INTERLEAVED_ATTRIBS); 

    if (!Finalize()) {
        return false;
    }

    m_deltaTimeMillisLocation = GetUniformLocation("gDeltaTimeMillis");
    m_randomTextureLocation = GetUniformLocation("gRandomTexture");
    m_timeLocation = GetUniformLocation("gTime");
    m_launcherLifetimeLocation = GetUniformLocation("gLauncherLifetime");
    m_shellLifetimeLocation = GetUniformLocation("gShellLifetime");
    m_secondaryShellLifetimeLocation = GetUniformLocation("gSecondaryShellLifetime");

    if (m_deltaTimeMillisLocation == INVALID_UNIFORM_LOCATION ||
        m_timeLocation == INVALID_UNIFORM_LOCATION ||
        m_randomTextureLocation == INVALID_UNIFORM_LOCATION) {
        m_launcherLifetimeLocation == INVALID_UNIFORM_LOCATION ||
        m_shellLifetimeLocation == INVALID_UNIFORM_LOCATION ||
        m_secondaryShellLifetimeLocation == INVALID_UNIFORM_LOCATION) {
        return false;
    }

    return true;
}

現在應該大致瞭解了我們為什麼要建立transform feedback物件,將一個緩衝附加到這個物件並且將場景渲染到這個快取中。但這仍然有一個問題:到底是什麼進入到了feedback快取中?是一個完整的頂點?我們可以指定頂點屬性的一個子集麼?它們之間的順序是怎樣的?上面的程式碼解釋了這些問題,這個函式用於初始化PSUpdateTechique,這個類是負責對粒子屬性進行更新的著色器。我們在glBeginTransformFeedback()和glEndTransformFeedback()之間使用它。為了指定要輸入進快取中的屬性,我們需要在著色器程式物件連結之前呼叫glTransformFeedbackVaryings()。這個函式有四個引數:程式物件、屬性名的字串陣列、在陣列中的字串數量、以及一個標記值(GL_INTERLEAVED_ATTRIBS或者GL_SEPARATE_ATTRIBS)。陣列中的字串都必須上一個著色器階段輸出的屬性的名字(必須是在FS之前,可以是VS或者GS)。當 transform feedback被啟用的時候,每個頂點的這些屬性將被寫進快取中。屬性的順序和陣列中的順序一樣。至於glTransformFeedbackVaryings()函式的最後的一個引數是告訴OpenGL是把所有的屬性作為一個結構體輸入到一個快取中(GL_INTERLEAVER_ATTRIBS)還是把每一個屬性都輸出到單獨的快取中(GL_SEPARATE_ATTRIBS)中。如果你使用GL_SEPARATE_ATTRIBS,則只需要繫結一個快取即可;但是如果你使用GL_SEPARATE_ATTRIBS,那麼你需要為每一個屬性都繫結一個快取(繫結的位置需要與屬性的所在的槽相對應),快取繫結的位置可以通過glBindBufferBase()函式的第二個引數來指定。此外,繫結的快取的數量也是有明確限制的,其數量不允許超過GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS(通常是4)。

除了glTransformFeedbackVaryings(),其他的程式碼都是比較常見的。但是注意在這個著色器中我們並沒有使用片元著色器,因為我們在更新粒子的時候禁用了光柵化,所以我們不需要FS。

(ps_update.vs)

#version 330

layout (location = 0) in float Type;
layout (location = 1) in vec3 Position;
layout (location = 2) in vec3 Velocity;
layout (location = 3) in float Age;

out float Type0;
out vec3 Position0;
out vec3 Velocity0;
out float Age0;

void main()
{
    Type0 = Type;
    Position0 = Position;
    Velocity0 = Velocity;
    Age0 = Age;
}

這是負責粒子更新的頂點著色器程式碼。如你所見,這裡面十分的簡單,它所做的一切都是將頂點屬性傳遞到GS(重頭戲開始的地方)。

(ps_update.gs)

#version 330

layout(points) in;
layout(points) out;
layout(max_vertices = 30) out;

in float Type0[];
in vec3 Position0[];
in vec3 Velocity0[];
in float Age0[];

out float Type1;
out vec3 Position1;
out vec3 Velocity1;
out float Age1;

uniform float gDeltaTimeMillis;
uniform float gTime;
uniform sampler1D gRandomTexture;
uniform float gLauncherLifetime;
uniform float gShellLifetime;
uniform float gSecondaryShellLifetime;

#define PARTICLE_TYPE_LAUNCHER 0.0f
#define PARTICLE_TYPE_SHELL 1.0f
#define PARTICLE_TYPE_SECONDARY_SHELL 2.0f

在幾何著色器中,首先對我們需要的一些屬性進行了定義,它會接收一些頂點屬性,同時也會輸出一些頂點屬性。我們從VS中得到的所有屬性都會被輸出到transform feedback快取中(在進行一些處理之後)。同時這裡也聲明瞭一些一致變數,通過這些一致變數我們可以設定發射器的頻率,shell和secondary shell的生命週期(發射器根據它的頻率生成一個shell並且在shell的生命週期結束的時候,shell分裂成多個secondary shell)。

vec3 GetRandomDir(float TexCoord)
{
    vec3 Dir = texture(gRandomTexture, TexCoord).xyz;
    Dir -= vec3(0.5, 0.5, 0.5);
    return Dir;
}

我們使用這個函式來為shell生成一個隨機的的方向。方向被儲存在一個1D紋理(紋理的元素是浮點型的3D向量)。我們稍後將看見如何用隨機向量來填充紋理。這個函式僅僅只有一個浮點型別引數並且使用它來從紋理中取樣。因為在紋理中的所有的值都是在[0.0 - 1.0]之間,我們把取樣的結果減去向量[0.50.50.5],這樣做是為了把值的範圍對映到[-0.5 -0.5]之間,這樣獲得的向量就可以朝向任意方向了。

void main()
{
    float Age = Age0[0] + gDeltaTimeMillis;

    if (Type0[0] == PARTICLE_TYPE_LAUNCHER) {
        if (Age >= gLauncherLifetime) {
            Type1 = PARTICLE_TYPE_SHELL;
            Position1 = Position0[0];
            vec3 Dir = GetRandomDir(gTime/1000.0);
            Dir.y = max(Dir.y, 0.5);
            Velocity1 = normalize(Dir) / 20.0;
            Age1 = 0.0;
            EmitVertex();
            EndPrimitive();
            Age = 0.0;
        }

        Type1 = PARTICLE_TYPE_LAUNCHER;
        Position1 = Position0[0];
        Velocity1 = Velocity0[0];
        Age1 = Age;
        EmitVertex();
        EndPrimitive();
    }

GS的主函式包含粒子的處理過程。首先我們判斷粒子存在的時間是否到達其生命週期,然後再根據粒子的不同型別進行不同的處理。上面的程式碼處理髮射器粒子的情況。如果發射器的生命週期結束,我們生成一個 shell 粒子和把它送進transform feedback快取中。shell粒子以發射器的位置作為起始位置並通過從隨機紋理中進行取樣來獲得粒子的速度向量。我們使用全域性時間作為偽隨機數種子(雖然不是真正的隨機數但是還是能滿足我們的需要了),之後我們需要確保方向向量的Y值是大於等於0.5的,這樣shell粒子才會是始終朝向天空發射的。最後這個方向向量被標準化併除以20作為速度向量(你可以根據你自己的系統做一個調整)。新粒子的年齡當然是為0,我們也重設發射器的年齡使得發射器可以再次通過這個邏輯發射粒子。此外,我們總是會將發射器重新輸出到快取中(因為發射器始終都存在場景中的)。

    else {
        float DeltaTimeSecs = gDeltaTimeMillis / 1000.0f;
        float t1 = Age0[0] / 1000.0;
        float t2 = Age / 1000.0;
        vec3 DeltaP = DeltaTimeSecs * Velocity0[0];
        vec3 DeltaV = vec3(DeltaTimeSecs) * (0.0, -9.81, 0.0);

在我們開始處理shell和secondary shell之前,我們需要定義一些相關的變數。DeltaTimeSecs是存放將毫秒轉化為秒的間隔時間。我們也將粒子(t1)的舊年齡和粒子(t2)的新年齡轉換成秒為單位。在位置座標上的改變根據公式'position = time * velocity'來計算。最後我們通過重力向量乘以DeltaTimeSecs來計算速度的改變數,粒子在其產生的時候獲得一個速度向量,但是在這之後唯一影響它的就是重力(忽略風等)。在地球上一個下落物體的重力加速度為9.81,因為重力的方向是向下的,所以我們重力向量的Y分量設定為負,X和Z分量設定為0if (Type0[0] == PARTICLE_TYPE_SHELL) {
            if (Age < gShellLifetime) {
                Type1 = PARTICLE_TYPE_SHELL;
                Position1 = Position0[0] + DeltaP;
                Velocity1 = Velocity0[0] + DeltaV;
                Age1 = Age;
                EmitVertex();
                EndPrimitive();
            }
            else {
                for (int i = 0 ; i < 10 ; i++) {
                    Type1 = PARTICLE_TYPE_SECONDARY_SHELL;
                    Position1 = Position0[0];
                    vec3 Dir = GetRandomDir((gTime + i)/1000.0);
                    Velocity1 = normalize(Dir) / 20.0;
                    Age1 = 0.0f;
                    EmitVertex();
                    EndPrimitive();
                }
            }
        }

我們現在把注意力放在shell粒子的處理上。只要這個例子的年齡還沒有達到它的生命週期,它將會一直被儲存在系統中,我們只需要基於之前算的變化量來更新它的座標和速度即可。一旦它達到它的生命週期,它將會被刪除並且生成10個secondary粒子來代替它,之後把這十個粒子輸出到快取中。這些新生成的粒子都會獲得當前 shell粒子的位置屬性,至於速度則通過上面提到的方法來獲取一個隨機的速度向量。對於Secondary shell 粒子,我們不會限制其方向,它可以向任意方向發射,這樣看起來才會像是真的。

        else {
            if (Age < gSecondaryShellLifetime) {
                Type1 = PARTICLE_TYPE_SECONDARY_SHELL;
                Position1 = Position0[0] + DeltaP;
                Velocity1 = Velocity0[0] + DeltaV;
                Age1 = Age;
                EmitVertex();
                EndPrimitive();
            }
        }
    }
}

secondary shell的處理和shell的處理是相似的,不同的是當它達到生命週期的時候,它直接被刪除並且沒有新的粒子生成。

(random_texture.cpp:37)

bool RandomTexture::InitRandomTexture(unsigned int Size)
{
    Vector3f* pRandomData = new Vector3f[Size];

    for (unsigned int i = 0 ; i < Size ; i++) {
        pRandomData[i].x = RandomFloat();
        pRandomData[i].y = RandomFloat();
        pRandomData[i].z = RandomFloat();
    }

    glGenTextures(1, &m_textureObj);
    glBindTexture(GL_TEXTURE_1D, m_textureObj);
    glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, Size, 0.0f, GL_RGB, GL_FLOAT, pRandomData);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT); 

    delete [] pRandomData;

    return GLCheckError();
}

RandomTexture類是一個十分有用的工具,它能夠為著色器程式提供隨機資料。這是一個以GL_RGB為內部格式的1D紋理並且是浮點型別的。這意味著每一個元素是3個浮點型別值組成的一個向量。注意我們設定其覆蓋模式為GL_REPEAT。這允許我們使用任意的紋理座標來進行紋理紋理。如果紋理座標是超過1.0,它簡單的被折回到合理的範圍,所以它總是能夠得到一個合法的值。在這個教程中random texture都是被繫結在3號紋理單元上的。你可以看見在標頭檔案engine_common.h裡看到紋理單元的設定。