[OpenGL] 延遲渲染下的簡單透明渲染機制

reference:http://www.klayge.org/wiki/index.php/%E5%BB%B6%E8%BF%9F%E6%B8%B2%E6%9F%93

       本文主要描述了自己實現延遲渲染下的透明物體渲染機制的過程。

方案探索

       前提是已經實現了基本的延遲渲染框架,但還沒有支持透明物體的渲染。最近打算開始進行這一項工作。

       目前接觸到的一個比較常見的做法是,使用延遲渲染+前向渲染結合的方式。也就是說,透明物體依然走傳統的前向渲染的方式。這就意味着需要把延遲渲染計算各種複雜光照等代碼在前向渲染裏重新實現一遍。

       首先,我們需要明確一個問題,爲什麼延遲渲染不適用於透明物體:延遲渲染只計算了離視野最近的物體像素,並對其進行光照計算和着色。因此,這會導致:

        ● 透明物體和不透明物體重疊時,半透明物體在後,僅渲染透明物體,效果正確。

        ● 透明物體和不透明物體重疊時,半透明物體在前,僅渲染半透明物體,效果錯誤。

        ● 透明物體之間重疊時,僅渲染最前面的半透明物體,效果錯誤。

       此處參考了引用文章給出的透明渲染方案,概括而言,就是使用延遲渲染的框架,分別渲染不透明物體,透明物體背面,透明物體正面,再把三者按照alpha合併。

        在這種情況下,我們可以基本保證第二種情況的正確;而對於第三種情況而言,根據前面的描述,由於延遲渲染僅對離相機最近的像素進行光照/着色計算,我們依然只能計算(特別地,若最近的像素透明度爲0,我們忽略這一像素)最近物體的光照。 

        對於後者描述的情況,採取的解決方案是寫入G-Buffer時僅混合物體顏色,在延遲渲染過程中,依然只計算最近物體的光照,但把混合後的顏色作爲最近物體的基本顏色進行光照計算。

         這就相當於假設後面物體的光沒有透過半透明物體,但我們知道,之所以能看見物體,就是因爲物體反射的光進入了我們的眼睛,所以這實際上是不可能的,極端的例子就是當物體完全透明的時候,我們依然會按照這個看不見的物體來計算光照。

        通過這種方法,我們就得到了一個實現比較簡單,性能消耗較低,效果大致上還說得過去的半透明效果。

具體實現

        參考引用中的思路,我也簡單設計了一下我的透明渲染框架,整個流程大致概括如下:

         

        渲染隊列

        爲了控制不同物體的渲染,引入了渲染隊列機制。目前設定了如下選項:背景、默認、透明測試、疊加、透明混合(單面)、透明混合(雙面)。定義如下:

enum ERenderQueue
{
    ERQ_Background,
    ERQ_Default,
    ERQ_AlphaTest,
    ERQ_Overlay,
    ERQ_Transparent,
    ERQ_Transparent_TwoSide,
    ERQ_Num
};

 

        使用較爲簡單的數據結構來維護:

vector<unordered_set<Object*>>  renderQueue;
list<pair<Object*, bool>>       transparentObjs; 
// first : object, second : isTwoSideTransparent

        我們通過如下函數修改物體在渲染隊列中的位置:

void ObjectInfo::SetRenderQueue(Object* obj, int renderPriority)
{
    if(!obj)
    {
        return;
    }
    if(obj->renderPriority == renderPriority)
    {
        return;
    }

    bool bTransparentDirty = false;

    // 1: erase the old obj
    if(obj->renderPriority == ERQ_Transparent || obj->renderPriority == ERQ_Transparent_TwoSide)
    {
        bool bTwoSide = obj->renderPriority == ERQ_Transparent_TwoSide;
        auto it = find(transparentObjs.begin(), transparentObjs.end(),make_pair(obj, bTwoSide));
        if(it != transparentObjs.end())
        {
            transparentObjs.erase(it);
            bTransparentDirty = true;
        }
    }
    else if(obj->renderPriority >= 0)
    {
        size_t id = static_cast<size_t>(obj->renderPriority);
        renderQueue[id].erase(obj);
    }

    // 2: insert the new obj
    if(renderPriority == ERQ_Transparent || renderPriority == ERQ_Transparent_TwoSide)
    {
        bool bTwoSide = renderPriority == ERQ_Transparent_TwoSide;
        transparentObjs.push_back({obj, bTwoSide});
        bTransparentDirty = true;
    }
    else
    {
        size_t id = static_cast<size_t>(renderPriority);
        renderQueue[id].insert(obj);
    }

    // 3: update renderpriority
    obj->renderPriority = renderPriority;

    // 4: check update sort
    if(bTransparentDirty)
    {
        SortTransparentObjs();
    }
}

 

        寫入幀緩衝與混合

        首先,我們利用延遲渲染框架分別渲染不透明物體,透明物體背面,透明物體正面之後,將會得到三個屏幕大小的紋理。此我們需要分配一個屏幕大小的幀緩衝用於寫入紋理。由於這三個過程是獨立的(非同時進行的),因此每個過程可以共用同一個幀緩存。

        對於已有的三張紋理,我們可以有很多種方法實現混合操作:要麼在寫入幀緩衝的時候,與顏色緩衝區中已有的顏色自行計算混合,輸出計算後的顏色,之後,將紋理繪製到屏幕大小的四邊形上即可;要麼使用系統自帶的glEnable(GL_BLEND),但後者似乎有一個弊端,就是在有多個緩衝區的時候,會對所有緩衝區都進行blend操作,無法單獨控制每個通道的開關。

 

        半透明混合

        在繪製的過程中,我們完整地執行三次延遲渲染的操作。在第一次繪製不透明物體時,無需任何特殊操作;第二次繪製透明物體背面時,我們開啓正面剔除,關閉深度寫入;第三次繪製透明物體正面時,我們開啓背面剔除,關閉深度寫入。

        同理,由於這幾個過程是獨立的,我們也可以共享G-Buffer對應的空間,而無需額外分配幀緩衝。這意味着我們無需浪費3倍的帶寬和內存,就能直接在延遲渲染中引入透明渲染機制。但需要注意的是,切換過程中,G-Buffer中的顏色是可以清除的,但我們必須保留深度數據(或者以其它方式記錄正確的深度),避免深度測試出錯:

未保留非透明物體寫入的深度時,深度測試出現問題

        即使我們僥倖繞過了前向渲染,在不引入次序無關透明度算法的情況下,我們依然需要進行排序,以獲得正確的效果。假設我們使用如下的計算公式:

glBlendFunc(GL_SRC_ALPHA , GL_ONE_MINUS_SRC_ALPHA);

        默認情況下,混合計算時不會考慮物體的先後順序,它只會將當前寫入的像素和即將寫入的像素按照公式計算,在src和dst對調的時候,計算得到的顏色大概率是不正確的。因此,我們需要先繪製距離相機遠的物體,再繪製距離相機近的物體。

        最爲基礎的方式,是將物體視爲質點,根據這一位置,按照它在視圖空間的z值對透明物體進行排序。這裏我們實際上忽略了物體本身的複雜度,如果物體較爲複雜,一般的方法是將其拆分爲多個組件進行渲染。

bool transparentCmp(const pair<Object*, bool>& data1, const pair<Object*, bool>& data2)
{
    Object* obj1 = data1.first;
    Object* obj2 = data2.first;

    const QMatrix4x4& viewMat = Camera::Inst()->GetViewMatrix();
    QVector4D pos1 = viewMat * QVector4D(obj1->position.x, obj1->position.y, obj1->position.z, 1.0f);
    QVector4D pos2 = viewMat * QVector4D(obj2->position.x, obj2->position.y, obj2->position.z, 1.0f);

    return pos1.z()/pos1.w() < pos2.z()/pos2.w();
}

void ObjectInfo::SortTransparentObjs()
{
    transparentObjs.sort(transparentCmp);
}

        在這種情況下,我們需要在渲染隊列變化/相機視圖矩陣變化的時候更新排序,是一個比較消耗性能的過程:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章