延遲渲染中光源的體積光(Light Volumn)

這兩天在整理LearnOpenGL教程中延遲着色部分的內容,在3月份看Unity Shader入門精要這本書,涉及到這個內容,當時僅僅是一掃而過,沒有注意,這兩天的學習讓自己對前向渲染延遲渲染有了一個直觀的認識.

教程中提及了光體積(Light Volumn)的概念和使用技巧,但是並未給出示例代碼,自己在查閱資料整理出了該部分的代碼,作爲記錄,同時希望可以幫助在這裏卡殼的童鞋.

參考的鏈接:
1.LearnOpenGL教程

該篇博客的結構如下:

  1. 渲染球體實現光體積
  2. 模板緩衝實現光體積
  3. 遇到的一些問題

渲染球體實現光體積

效果
渲染的球體:
這裏寫圖片描述
渲染結果:
這裏寫圖片描述

思路

渲染一個實際的球體,並根據該光源的半徑縮放.該球體的中心凡在光源的位置,由於它是根據光體積半徑縮放的,這個球體正好覆蓋了光的可視體積.我們使用大體相同的延遲片段着色器來渲染球體,因爲球體產生了完全匹配於受影響像素的着色器調用,我們只渲染了受影響的像素而跳過其它的像素.

相比於教程中使用僞光體積(其所提及的方法由於GPU和GLSL在優化循環和分支上的不足,實際上和沒有光體積的效率一樣)的代碼,光照渲染階段的片段着色器的代碼沒有較大改動,僅僅是刪除了分支和計算距離的語句

#version 330 core

in VS_OUT
{
    vec2 texcoord;
}fs_in;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;

struct Light
{
    vec3 position;
    vec3 color;
    float radius;
    float linear;
    float quadratic;
};

const int NR_LIGHTS = 32;
uniform Light lights[NR_LIGHTS];
uniform vec3 viewPos;

out vec4 color;

void main()
{
    //從G緩衝中獲取數據
    vec3 position = texture2D(gPosition, fs_in.texcoord).rgb;
    vec3 normal = texture2D(gNormal, fs_in.texcoord).rgb;
    vec3 albedo = texture2D(gAlbedoSpec, fs_in.texcoord).rgb;

    //和往常一樣進行光照計算
    vec3 lighting = vec3(0.0f);
    vec3 viewDir = normalize(viewPos - position);
    for(int i = 0; i < NR_LIGHTS; ++i)
    {
        float distance = length(lights[i].position - position);
        float attenuation = 1.0f / (1.0f + lights[i].linear * distance + 
            lights[i].quadratic * distance * distance);

        lighting += attenuation * albedo * vec3(0.1) * lights[i].color; //環境光

        //漫反射
        vec3 lightDir = normalize(lights[i].position - position);
        float diff = max(dot(lightDir, normal), 0.0f);
        vec3 diffuse = attenuation * diff * albedo * lights[i].color;
        lighting += diffuse;

        //鏡面高光
        vec3 halfwayDir = normalize(viewDir + lightDir);
        float spec = pow(max(dot(halfwayDir, normal), 0.0f), 32.0f);
        vec3 specular = attenuation * spec * lights[i].color;
        lighting += specular;
    }

    color = vec4(vec3(lighting), 1.0f);
}

光照渲染階段的頂點着色器有一點改動,

#version 330 core

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texcoord;

layout (std140) uniform Camera
{
    mat4 view;
    mat4 projection;
};

uniform mat4 model;

out VS_OUT
{
    vec2 texcoord;
}vs_out;

void main()
{
    vec4 position = projection * view * model * vec4(position, 1.0f);
    gl_Position = position;
    vs_out.texcoord = vec2(position.x / position.w, position.y / position.w) * 0.5 + 0.5;
}

如上代碼,相比於教程中的頂點着色器,它將頂點的紋理座標直接傳給片段着色器,由於我們要限定渲染的區域爲球體所包含的區域,所以這個區域必定是和球體的頂點座標相關的,於是我們將經過MVP變換後的頂點座標position.x,position.y除以position.w得到標準化的頂點座標,其座標範圍是在[-1, 1]區間,並以此座標來對幾何處理後獲取的位置,顏色,法向量等紋理進行採樣.需要注意的是,紋理的座標是[0,1]區間,所以需要” * 0.5 + 0.5”來將座標從[-1, 1]變換到[0, 1].

由於各個光源的光體積可能有重合的地方,對於重合的部分,如果只是簡單的採用後渲染的光源的數據,可能會出現錯誤,畢竟各個光源的屬性可能是不同的,以上粗暴的方式明顯不理想.

這裏,我使用融合來解決這個問題,在繪製光體積的時候,開啓融合,且配置融合的參數如下:

glBlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE);
glBlendEquation(GL_MAX);

這樣,對於重疊的區域,渲染管線會選擇更亮的光照結果作爲渲染結果,這是符合我們的期望的.

該部分的細代碼可見文末的全部代碼的鏈接.

問題
教程中提到,”該方法需要開啓面剔除(不然會渲染一個光效果兩次),但開啓後,用戶可能進入一個光源的光體積,然後這樣之後這個體積就不再被渲染(由於背面剔除),這會使光源的影響消失”,後半句所說的光源的影響會消失,我沒有想明白,運行的結果好像也不存在這個問題,還望有大大賜教.

2017.6.4更新
昨日和今日參加了網易遊戲的線下探營活動,這次活動乾貨滿滿,更加想加入網易遊戲了~
廢話說完了~

針對之前教程中提到的”該方法需要開啓面剔除(不然會渲染一個光效果兩次),但開啓後,用戶可能進入一個光源的光體積,然後這樣之後這個體積就不再被渲染(由於背面剔除),這會使光源的影響消失”,後半句本來是無法理解的,但在探營活動中,和一位其它實驗室的同學交流後,弄明白了後半句的意思,如下圖,
這裏寫圖片描述
這裏寫圖片描述
上圖是攝像機在球體外時的一個側面圖,此時黑色半圓爲front面,紅色半圓爲back面,如果不開啓面剔除,那麼兩個半圓均需渲染,這樣的結果是,對於兩個半圓投影在攝像機視角平面P會被渲染兩次(即黑色半圓投影的片段(P平面)會被渲染一次,紅色半圓投影的片段(P平面)也會被渲染一次)造成P平面上的每個片段會被執行兩次片段着色器;開啓面剔除後,紅色半圓會被剔除,只有黑色半圓執行投影的片段會執行片段着色器代碼,這在第一個圖中是正確的
但如果攝像機的位置進入球體內,那麼由於渲染管線會將攝像機視角外的物體執行裁剪操作,結果是,黑色半圓此時不可見,而紅色半圓由於面剔除(可以看看面剔除的原理)也不會被渲染,結果是,P平面本身在攝像機位置是可見的,但是由於進入球體後,黑紅半圓都不會被渲染了,P平面也不會被渲染,由此導致錯誤!
在實際的代碼中,將攝像機位置放置在球體內後,上文中P平面的內容確實沒有再渲染,證明了上述的猜測!
之前沒有理解這句話的原因,還是對渲染管線中的一些細節知識掌握不足.

模板緩衝實現光體積

效果
渲染的區域(光源的光體積):

這裏寫圖片描述

最終效果:
這裏寫圖片描述

思路

glEnable(GL_STENCIL_TEST);  //開啓模板測試
glDisable(GL_DEPTH_TEST);   //關閉深度測試

glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  //配置模板測試
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);    //設置模板緩衝爲可寫狀態

RenderLightSphere   //渲染光源對應的球體

glEnable(GL_DEPTH_TEST);    //恢復深度測試
glStencilFunc(GL_EQUAL, 1, 0xFF);   //配置模板測試,此時僅有先前球體繪製的區域才能通過模板測試
glStencilMask(0x00);    //禁止修改模板緩衝

渲染幾何階段獲取的紋理至一個平面上(和教程中的繪製代碼相同)

glStencilMask(0xFF);    //恢復模板緩衝可寫,如果沒有這行,運行結果會和沒有清空緩衝區類似,不明白其中原因

基本上,就是模板技術的簡單應用,詳細代碼可見文末的全部代碼的鏈接.

思考
我們知道,模板測試是在片段着色器執行完畢後進行的,也就是說,其實即使是光源照不到的片段,也進行了費時的光照計算,這明顯不是光體積的目的.
我不知道,是不是自己對教程中提到的模板測試的方法的理解有問題,還望指出,在此非常感謝.

遇到的一些問題

1.幀緩衝由顏色緩衝深度緩衝模板緩衝構成,在未涉及自定義的幀緩衝時,窗口庫(例如glfw)會爲我們創建好深度緩衝和模板緩衝,這樣我們在使用的時候,只要直接啓動深度緩衝或者模板緩衝即可,但是在我們自己創建幀緩衝時,如果還是簡單的開啓深度緩衝或模板緩衝,運行的結果將和未開啓深度緩衝一樣,其中的原因是,我們此時必須顯式地去創建深度緩衝和模板緩衝,並將他們綁定到創建的幀緩衝對象上,這樣深度信息或者模板信息纔有存放的媒介;

2.深度緩衝和模板緩衝的創建,如下:

//深度緩衝,如果沒有會無法進行GL_DEPTH_TEST
_depthStencilTexture.setInternalFormat(GL_DEPTH_COMPONENT);
_depthStencilTexture.setImageFormat(GL_DEPTH_COMPONENT);
_depthStencilTexture.setDataType(GL_FLOAT);
_depthStencilTexture.setWrapType(GL_CLAMP_TO_BORDER);
_depthStencilTexture.generate(SCREEN_WIDTH, SCREEN_HEIGHT, nullptr);

上述代碼和創建深度緩衝的代碼無異,但是我們要開啓模板緩衝時,並不需要創建模板緩衝區,實際上,我在按照OpenGL上的API去創建模板緩衝後,在將模板緩衝綁定到幀緩衝對象時,反倒會出現問題.我想,可能是OpenGL自動將GL_FLOAT中的24位分配給深度緩衝,剩餘的8位分配給了模板緩衝,所以不需要自己再額外去創建模板緩衝.

全部代碼鏈接,DeferedRender類

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