OpenGL 圖形庫的使用(四十一)—— 高級光照之SSAO

版本記錄

版本號 時間
V1.0 2018.01.19

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界。
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級OpenGL之高級數據Advanced Data
28. OpenGL 圖形庫的使用(二十八)—— 高級OpenGL之高級GLSL Advanced GLSL
29. OpenGL 圖形庫的使用(二十九)—— 高級OpenGL之幾何著色器Geometry Shader
30. OpenGL 圖形庫的使用(三十)—— 高級OpenGL之實例化Instancing
31. OpenGL 圖形庫的使用(三十一)—— 高級OpenGL之抗鋸齒Anti Aliasing
32. OpenGL 圖形庫的使用(三十二)—— 高級光照之高級光照Advanced Lighting
33. OpenGL 圖形庫的使用(三十三)—— 高級光照之Gamma校正Gamma Correction
34. OpenGL 圖形庫的使用(三十四)—— 高級光照之陰影 - 陰影映射Shadow Mapping
35. OpenGL 圖形庫的使用(三十五)—— 高級光照之陰影 - 點陰影Point Shadows
36. OpenGL 圖形庫的使用(三十六)—— 高級光照之法線貼圖Normal Mapping
37. OpenGL 圖形庫的使用(三十七)—— 高級光照之視差貼圖Parallax Mapping
38. OpenGL 圖形庫的使用(三十八)—— 高級光照之HDR
39. OpenGL 圖形庫的使用(三十九)—— 高級光照之泛光
40. OpenGL 圖形庫的使用(四十)—— 高級光照之延遲著色法Deferred Shading

SSAO

我們已經在前面的基礎教程中簡單介紹到了這部分內容:環境光照(Ambient Lighting)。環境光照是我們加入場景總體光照中的一個固定光照常量,它被用來模擬光的散射(Scattering)。在現實中,光線會以任意方向散射,它的強度是會一直改變的,所以間接被照到的那部分場景也應該有變化的強度,而不是一成不變的環境光。其中一種間接光照的模擬叫做環境光遮蔽(Ambient Occlusion),它的原理是通過將褶皺、孔洞和非??拷膲γ孀儼档姆椒ń颇M出間接光照。這些區域很大程度上是被周圍的幾何體遮蔽的,光線會很難流失,所以這些地方看起來會更暗一些。站起來看一看你房間的拐角或者是褶皺,是不是這些地方會看起來有一點暗?

下面這幅圖展示了在使用和不使用SSAO時場景的不同。特別注意對比褶皺部分,你會發現(環境)光被遮蔽了許多:

盡管這不是一個非常明顯的效果,啟用SSAO的圖像確實給我們更真實的感覺,這些小的遮蔽細節給整個場景帶來了更強的深度感。

環境光遮蔽這一技術會帶來很大的性能開銷,因為它還需要考慮周圍的幾何體。我們可以對空間中每一點發射大量光線來確定其遮蔽量,但是這在實時運算中會很快變成大問題。在2007年,Crytek公司發布了一款叫做屏幕空間環境光遮蔽(Screen-Space Ambient Occlusion, SSAO)的技術,并用在了他們的看家作孤島危機上。這一技術使用了屏幕空間場景的深度而不是真實的幾何體數據來確定遮蔽量。這一做法相對于真正的環境光遮蔽不但速度快,而且還能獲得很好的效果,使得它成為近似實時環境光遮蔽的標準。

SSAO背后的原理很簡單:對于鋪屏四邊形(Screen-filled Quad)上的每一個片段,我們都會根據周邊深度值計算一個遮蔽因子(Occlusion Factor)。這個遮蔽因子之后會被用來減少或者抵消片段的環境光照分量。遮蔽因子是通過采集片段周圍球型核心(Kernel)的多個深度樣本,并和當前片段深度值對比而得到的。高于片段深度值樣本的個數就是我們想要的遮蔽因子。

上圖中在幾何體內灰色的深度樣本都是高于片段深度值的,他們會增加遮蔽因子;幾何體內樣本個數越多,片段獲得的環境光照也就越少。

很明顯,渲染效果的質量和精度與我們采樣的樣本數量有直接關系。如果樣本數量太低,渲染的精度會急劇減少,我們會得到一種叫做波紋(Banding)的效果;如果它太高了,反而會影響性能。我們可以通過引入隨機性到采樣核心(Sample Kernel)的采樣中從而減少樣本的數目。通過隨機旋轉采樣核心,我們能在有限樣本數量中得到高質量的結果。然而這仍然會有一定的麻煩,因為隨機性引入了一個很明顯的噪聲圖案,我們將需要通過模糊結果來修復這一問題。下面這幅圖片(John Chapman的佛像)展示了波紋效果還有隨機性造成的效果:

你可以看到,盡管我們在低樣本數的情況下得到了很明顯的波紋效果,引入隨機性之后這些波紋效果就完全消失了。

Crytek公司開發的SSAO技術會產生一種特殊的視覺風格。因為使用的采樣核心是一個球體,它導致平整的墻面也會顯得灰蒙蒙的,因為核心中一半的樣本都會在墻這個幾何體上。下面這幅圖展示了孤島危機的SSAO,它清晰地展示了這種灰蒙蒙的感覺:

由于這個原因,我們將不會使用球體的采樣核心,而使用一個沿著表面法向量的半球體采樣核心。

通過在法向半球體(Normal-oriented Hemisphere)周圍采樣,我們將不會考慮到片段底部的幾何體.它消除了環境光遮蔽灰蒙蒙的感覺,從而產生更真實的結果。這個SSAO教程將會基于法向半球法和John Chapman出色的SSAO教程。


樣本緩沖

SSAO需要獲取幾何體的信息,因為我們需要一些方式來確定一個片段的遮蔽因子。對于每一個片段,我們將需要這些數據:

  • 逐片段位置向量
  • 逐片段的法線向量
  • 逐片段的反射顏色
  • 采樣核心
  • 用來旋轉采樣核心的隨機旋轉矢量

通過使用一個逐片段觀察空間位置,我們可以將一個采樣半球核心對準片段的觀察空間表面法線。對于每一個核心樣本我們會采樣線性深度紋理來比較結果。采樣核心會根據旋轉矢量稍微偏轉一點;我們所獲得的遮蔽因子將會之后用來限制最終的環境光照分量。

由于SSAO是一種屏幕空間技巧,我們對鋪屏2D四邊形上每一個片段計算這一效果;也就是說我們沒有場景中幾何體的信息。我們能做的只是渲染幾何體數據到屏幕空間紋理中,我們之后再會將此數據發送到SSAO著色器中,之后我們就能訪問到這些幾何體數據了。如果你看了前面一篇教程,你會發現這和延遲渲染很相似。這也就是說SSAO和延遲渲染能完美地兼容,因為我們已經存位置和法線向量到G緩沖中了。

在這個教程中,我們將會在一個簡化版本的延遲渲染器(延遲著色法教程中)的基礎上實現SSAO,所以如果你不知道什么是延遲著色法,請先讀完那篇教程。

由于我們已經有了逐片段位置和法線數據(G緩沖中),我們只需要更新一下幾何著色器,讓它包含片段的線性深度就行了?;貞浳覀冊谏疃葴y試那一節學過的知識,我們可以從gl_FragCoord.z中提取線性深度:

#version 330 core
layout (location = 0) out vec4 gPositionDepth;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;

const float NEAR = 0.1; // 投影矩陣的近平面
const float FAR = 50.0f; // 投影矩陣的遠平面
float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // 回到NDC
    return (2.0 * NEAR * FAR) / (FAR + NEAR - z * (FAR - NEAR));    
}

void main()
{    
    // 儲存片段的位置矢量到第一個G緩沖紋理
    gPositionDepth.xyz = FragPos;
    // 儲存線性深度到gPositionDepth的alpha分量
    gPositionDepth.a = LinearizeDepth(gl_FragCoord.z); 
    // 儲存法線信息到G緩沖
    gNormal = normalize(Normal);
    // 和漫反射顏色
    gAlbedoSpec.rgb = vec3(0.95);
}

提取出來的線性深度是在觀察空間中的,所以之后的運算也是在觀察空間中。確保G緩沖中的位置和法線都在觀察空間中(乘上觀察矩陣也一樣)。觀察空間線性深度值之后會被保存在gPositionDepth顏色緩沖的alpha分量中,省得我們再聲明一個新的顏色緩沖紋理。

通過一些小技巧來通過深度值重構實際位置值是可能的,Matt Pettineo在他的博客里提到了這一技巧。這一技巧需要在著色器里進行一些計算,但是省了我們在G緩沖中存儲位置數據,從而省了很多內存。為了示例的簡單,我們將不會使用這些優化技巧,你可以自行探究。

gPositionDepth顏色緩沖紋理被設置成了下面這樣:

glGenTextures(1, &gPositionDepth);
glBindTexture(GL_TEXTURE_2D, gPositionDepth);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

這給我們了一個線性深度紋理,我們可以用它來對每一個核心樣本獲取深度值。注意我們把線性深度值存儲為了浮點數據;這樣從0.1到50.0范圍深度值都不會被限制在[0.0, 1.0]之間了。如果你不用浮點值存儲這些深度數據,確保你首先將值除以FAR來標準化它們,再存儲到gPositionDepth紋理中,并在以后的著色器中用相似的方法重建它們。同樣需要注意的是GL_CLAMP_TO_EDGE的紋理封裝方法。這保證了我們不會不小心采樣到在屏幕空間中紋理默認坐標區域之外的深度值。

接下來我們需要真正的半球采樣核心和一些方法來隨機旋轉它。


法向半球

我們需要沿著表面法線方向生成大量的樣本。就像我們在這個教程的開始介紹的那樣,我們想要生成形成半球形的樣本。由于對每個表面法線方向生成采樣核心非常困難,也不合實際,我們將在切線空間(Tangent Space)內生成采樣核心,法向量將指向正z方向。

假設我們有一個單位半球,我們可以獲得一個擁有最大64樣本值的采樣核心:

std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0); // 隨機浮點數,范圍0.0 - 1.0
std::default_random_engine generator;
std::vector<glm::vec3> ssaoKernel;
for (GLuint i = 0; i < 64; ++i)
{
    glm::vec3 sample(
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator)
    );
    sample = glm::normalize(sample);
    sample *= randomFloats(generator);
    GLfloat scale = GLfloat(i) / 64.0; 
    ssaoKernel.push_back(sample);  
}

我們在切線空間中以-1.0到1.0為范圍變換x和y方向,并以0.0和1.0為范圍變換樣本的z方向(如果以-1.0到1.0為范圍,取樣核心就變成球型了)。由于采樣核心將會沿著表面法線對齊,所得的樣本矢量將會在半球里。

目前,所有的樣本都是平均分布在采樣核心里的,但是我們更愿意將更多的注意放在靠近真正片段的遮蔽上,也就是將核心樣本靠近原點分布。我們可以用一個加速插值函數實現它:

 ...[接上函數]
   scale = lerp(0.1f, 1.0f, scale * scale);
   sample *= scale;
   ssaoKernel.push_back(sample);  
}

lerp被定義為:

GLfloat lerp(GLfloat a, GLfloat b, GLfloat f)
{
    return a + f * (b - a);
}

這就給了我們一個大部分樣本靠近原點的核心分布。

每個核心樣本將會被用來偏移觀察空間片段位置從而采樣周圍的幾何體。我們在教程開始的時候看到,如果沒有變化采樣核心,我們將需要大量的樣本來獲得真實的結果。通過引入一個隨機的轉動到采樣核心中,我們可以很大程度上減少這一數量。


隨機核心轉動

通過引入一些隨機性到采樣核心上,我們可以大大減少獲得不錯結果所需的樣本數量。我們可以對場景中每一個片段創建一個隨機旋轉向量,但這會很快將內存耗盡。所以,更好的方法是創建一個小的隨機旋轉向量紋理平鋪在屏幕上。

我們創建一個4x4朝向切線空間平面法線的隨機旋轉向量數組:

std::vector<glm::vec3> ssaoNoise;
for (GLuint i = 0; i < 16; i++)
{
    glm::vec3 noise(
        randomFloats(generator) * 2.0 - 1.0, 
        randomFloats(generator) * 2.0 - 1.0, 
        0.0f); 
    ssaoNoise.push_back(noise);
}

由于采樣核心實驗者正z方向在切線空間內旋轉,我們設定z分量為0.0,從而圍繞z軸旋轉。

我們接下來創建一個包含隨機旋轉向量的4x4紋理;記得設定它的封裝方法為GL_REPEAT,從而保證它合適地平鋪在屏幕上。

GLuint noiseTexture; 
glGenTextures(1, &noiseTexture);
glBindTexture(GL_TEXTURE_2D, noiseTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

現在我們有了所有的相關輸入數據,接下來我們需要實現SSAO。


SSAO著色器

SSAO著色器在2D的鋪屏四邊形上運行,它對于每一個生成的片段計算遮蔽值(為了在最終的光照著色器中使用)。由于我們需要存儲SSAO階段的結果,我們還需要在創建一個幀緩沖對象:

GLuint ssaoFBO;
glGenFramebuffers(1, &ssaoFBO);  
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
GLuint ssaoColorBuffer;

glGenTextures(1, &ssaoColorBuffer);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0);

由于環境遮蔽的結果是一個灰度值,我們將只需要紋理的紅色分量,所以我們將顏色緩沖的內部格式設置為GL_RED

渲染SSAO完整的過程會像這樣:

// 幾何處理階段: 渲染到G緩沖中
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    [...]
glBindFramebuffer(GL_FRAMEBUFFER, 0);  

// 使用G緩沖渲染SSAO紋理
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
    glClear(GL_COLOR_BUFFER_BIT);
    shaderSSAO.Use();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, gPositionDepth);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);
    SendKernelSamplesToShader();
    glUniformMatrix4fv(projLocation, 1, GL_FALSE, glm::value_ptr(projection));
    RenderQuad();
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 光照處理階段: 渲染場景光照
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass.Use();
[...]
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
[...]
RenderQuad();

shaderSSAO這個著色器將對應G緩沖紋理(包括線性深度),噪聲紋理和法向半球核心樣本作為輸入參數:

#version 330 core
out float FragColor;
in vec2 TexCoords;

uniform sampler2D gPositionDepth;
uniform sampler2D gNormal;
uniform sampler2D texNoise;

uniform vec3 samples[64];
uniform mat4 projection;

// 屏幕的平鋪噪聲紋理會根據屏幕分辨率除以噪聲大小的值來決定
const vec2 noiseScale = vec2(800.0/4.0, 600.0/4.0); // 屏幕 = 800x600

void main()
{
    [...]
}

注意我們這里有一個noiseScale的變量。我們想要將噪聲紋理平鋪(Tile)在屏幕上,但是由于TexCoords的取值在0.0和1.0之間,texNoise紋理將不會平鋪。所以我們將通過屏幕分辨率除以噪聲紋理大小的方式計算TexCoords的縮放大小,并在之后提取相關輸入向量的時候使用。

vec3 fragPos = texture(gPositionDepth, TexCoords).xyz;
vec3 normal = texture(gNormal, TexCoords).rgb;
vec3 randomVec = texture(texNoise, TexCoords * noiseScale).xyz;

由于我們將texNoise的平鋪參數設置為GL_REPEAT,隨機的值將會在全屏不斷重復。加上fragPog和normal向量,我們就有足夠的數據來創建一個TBN矩陣,將向量從切線空間變換到觀察空間。

vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
vec3 bitangent = cross(normal, tangent);
mat3 TBN = mat3(tangent, bitangent, normal);

通過使用一個叫做Gramm-Schmidt處理(Gramm-Schmidt Process)的過程,我們創建了一個正交基(Orthogonal Basis),每一次它都會根據randomVec的值稍微傾斜。注意因為我們使用了一個隨機向量來構造切線向量,我們沒必要有一個恰好沿著幾何體表面的TBN矩陣,也就是不需要逐頂點切線(和雙切)向量。

接下來我們對每個核心樣本進行迭代,將樣本從切線空間變換到觀察空間,將它們加到當前像素位置上,并將片段位置深度與儲存在原始深度緩沖中的樣本深度進行比較。我們來一步步討論它:

float occlusion = 0.0;
for(int i = 0; i < kernelSize; ++i)
{
    // 獲取樣本位置
    vec3 sample = TBN * samples[i]; // 切線->觀察空間
    sample = fragPos + sample * radius; 

    [...]
}

這里的``kernelSize和radius變量都可以用來調整效果;在這里我們分別保持他們的默認值為64和1.0。對于每一次迭代我們首先變換各自樣本到觀察空間。之后我們會加觀察空間核心偏移樣本到觀察空間片段位置上;最后再用radius乘上偏移樣本來增加(或減少)SSAO的有效取樣半徑。

接下來我們變換sample到屏幕空間,從而我們可以就像正在直接渲染它的位置到屏幕上一樣取樣sample的(線性)深度值。由于這個向量目前在觀察空間,我們將首先使用projection矩陣uniform變換它到裁剪空間。

vec4 offset = vec4(sample, 1.0);
offset = projection * offset; // 觀察->裁剪空間
offset.xyz /= offset.w; // 透視劃分
offset.xyz = offset.xyz * 0.5 + 0.5; // 變換到0.0 - 1.0的值域

在變量被變換到裁剪空間之后,我們用xyz分量除以w分量進行透視劃分。結果所得的標準化設備坐標之后變換到[0.0, 1.0]范圍以便我們使用它們去取樣深度紋理:

float sampleDepth = -texture(gPositionDepth, offset.xy).w;

我們使用offset向量的x和y分量采樣線性深度紋理從而獲取樣本位置從觀察者視角的深度值(第一個不被遮蔽的可見片段)。我們接下來檢查樣本的當前深度值是否大于存儲的深度值,如果是的,添加到最終的貢獻因子上。

occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0);

這并沒有完全結束,因為仍然還有一個小問題需要考慮。當檢測一個靠近表面邊緣的片段時,它將會考慮測試表面之下的表面的深度值;這些值將會(不正確地)音響遮蔽因子。我們可以通過引入一個范圍檢測從而解決這個問題,正如下圖所示(John Chapman的佛像):

我們引入一個范圍測試從而保證我們只當被測深度值在取樣半徑內時影響遮蔽因子。將代碼最后一行換成:

float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
occlusion += (sampleDepth >= sample.z ? 1.0 : 0.0) * rangeCheck;    

這里我們使用了GLSL的smoothstep函數,它非常光滑地在第一和第二個參數范圍內插值了第三個參數。如果深度差因此最終取值在radius之間,它們的值將會光滑地根據下面這個曲線插值在0.0和1.0之間:

如果我們使用一個在深度值在radius之外就突然移除遮蔽貢獻的硬界限范圍檢測(Hard Cut-off Range Check),我們將會在范圍檢測應用的地方看見一個明顯的(很難看的)邊緣。

最后一步,我們需要將遮蔽貢獻根據核心的大小標準化,并輸出結果。注意我們用1.0減去了遮蔽因子,以便直接使用遮蔽因子去縮放環境光照分量。

}
occlusion = 1.0 - (occlusion / kernelSize);
FragColor = occlusion;  

下面這幅圖展示了我們最喜歡的納米裝模型正在打盹的場景,環境遮蔽著色器產生了以下的紋理:

可見,環境遮蔽產生了非常強烈的深度感。僅僅通過環境遮蔽紋理我們就已經能清晰地看見模型一定躺在地板上而不是浮在空中。

現在的效果仍然看起來不是很完美,由于重復的噪聲紋理再圖中清晰可見。為了創建一個光滑的環境遮蔽結果,我們需要模糊環境遮蔽紋理。


環境遮蔽模糊

在SSAO階段和光照階段之間,我們想要進行模糊SSAO紋理的處理,所以我們又創建了一個幀緩沖對象來儲存模糊結果。

GLuint ssaoBlurFBO, ssaoColorBufferBlur;
glGenFramebuffers(1, &ssaoBlurFBO);
glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
glGenTextures(1, &ssaoColorBufferBlur);
glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0);

由于平鋪的隨機向量紋理保持了一致的隨機性,我們可以使用這一性質來創建一個簡單的模糊著色器:

#version 330 core
in vec2 TexCoords;
out float fragColor;

uniform sampler2D ssaoInput;

void main() {
    vec2 texelSize = 1.0 / vec2(textureSize(ssaoInput, 0));
    float result = 0.0;
    for (int x = -2; x < 2; ++x) 
    {
        for (int y = -2; y < 2; ++y) 
        {
            vec2 offset = vec2(float(x), float(y)) * texelSize;
            result += texture(ssaoInput, TexCoords + offset).r;
        }
    }
    fragColor = result / (4.0 * 4.0);
}

這里我們遍歷了周圍在-2.0和2.0之間的SSAO紋理單元(Texel),采樣與噪聲紋理維度相同數量的SSAO紋理。我們通過使用返回vec2紋理維度的textureSize,根據紋理單元的真實大小偏移了每一個紋理坐標。我們平均所得的結果,獲得一個簡單但是有效的模糊效果:

這就完成了,一個包含逐片段環境遮蔽數據的紋理;在光照處理階段中可以直接使用。


應用環境遮蔽

應用遮蔽因子到光照方程中極其簡單:我們要做的只是將逐片段環境遮蔽因子乘到光照環境分量上。如果我們使用上個教程中的Blinn-Phong延遲光照著色器并做出一點修改,我們將會得到下面這個片段著色器:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;

uniform sampler2D gPositionDepth;
uniform sampler2D gNormal;
uniform sampler2D gAlbedo;
uniform sampler2D ssao;

struct Light {
    vec3 Position;
    vec3 Color;

    float Linear;
    float Quadratic;
    float Radius;
};
uniform Light light;

void main()
{             
    // 從G緩沖中提取數據
    vec3 FragPos = texture(gPositionDepth, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Diffuse = texture(gAlbedo, TexCoords).rgb;
    float AmbientOcclusion = texture(ssao, TexCoords).r;

    // Blinn-Phong (觀察空間中)
    vec3 ambient = vec3(0.3 * AmbientOcclusion); // 這里我們加上遮蔽因子
    vec3 lighting  = ambient; 
    vec3 viewDir  = normalize(-FragPos); // Viewpos 為 (0.0.0),在觀察空間中
    // 漫反射
    vec3 lightDir = normalize(light.Position - FragPos);
    vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Diffuse * light.Color;
    // 鏡面
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(Normal, halfwayDir), 0.0), 8.0);
    vec3 specular = light.Color * spec;
    // 衰減
    float dist = length(light.Position - FragPos);
    float attenuation = 1.0 / (1.0 + light.Linear * dist + light.Quadratic * dist * dist);
    diffuse  *= attenuation;
    specular *= attenuation;
    lighting += diffuse + specular;

    FragColor = vec4(lighting, 1.0);
}

(除了將其改到觀察空間)對比于之前的光照實現,唯一的真正改動就是場景環境分量與AmbientOcclusion值的乘法。通過在場景中加入一個淡藍色的點光源,我們將會得到下面這個結果:

你可以在這里找到完整的源代碼,和以下著色器:

// GLEW
#define GLEW_STATIC
#include <GL/glew.h>

// GLFW
#include <GLFW/glfw3.h>

// GL includes
#include <learnopengl/shader.h>
#include <learnopengl/camera.h>
#include <learnopengl/model.h>

// GLM Mathemtics
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// Other Libs
#include <SOIL.h>

#include <random> // necessary for generation of random floats (for sample kernel and noise texture)

// Properties
const GLuint SCR_WIDTH = 800, SCR_HEIGHT = 600;

// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void Do_Movement();
void RenderCube();
void RenderQuad();

// Camera
Camera camera(glm::vec3(0.0f, 0.0f, 5.0f));

// Delta
GLfloat deltaTime = 0.0f;
GLfloat lastFrame = 0.0f;

// Options
GLuint draw_mode = 1;

GLfloat lerp(GLfloat a, GLfloat b, GLfloat f)
{
    return a + f * (b - a);
}

// The MAIN function, from here we start our application and run our Game loop
int main()
{
    // Init GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr); // Windowed
    glfwMakeContextCurrent(window);

    // Set the required callback functions
    glfwSetKeyCallback(window, key_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // Options
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // Initialize GLEW to setup the OpenGL Function pointers
    glewExperimental = GL_TRUE;
    glewInit();
    glGetError();

    // Define the viewport dimensions
    glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);

    // Setup some OpenGL options
    glEnable(GL_DEPTH_TEST);

    // Setup and compile our shaders
    Shader shaderGeometryPass("ssao_geometry.vs", "ssao_geometry.frag");
    Shader shaderLightingPass("ssao.vs", "ssao_lighting.frag");
    Shader shaderSSAO("ssao.vs", "ssao.frag");
    Shader shaderSSAOBlur("ssao.vs", "ssao_blur.frag");

    // Set samplers
    shaderLightingPass.Use();
    glUniform1i(glGetUniformLocation(shaderLightingPass.Program, "gPositionDepth"), 0);
    glUniform1i(glGetUniformLocation(shaderLightingPass.Program, "gNormal"), 1);
    glUniform1i(glGetUniformLocation(shaderLightingPass.Program, "gAlbedo"), 2); 
    glUniform1i(glGetUniformLocation(shaderLightingPass.Program, "ssao"), 3); 
    shaderSSAO.Use();
    glUniform1i(glGetUniformLocation(shaderSSAO.Program, "gPositionDepth"), 0);
    glUniform1i(glGetUniformLocation(shaderSSAO.Program, "gNormal"), 1);
    glUniform1i(glGetUniformLocation(shaderSSAO.Program, "texNoise"), 2);

    // Objects
    Model nanosuit("../../../resources/objects/nanosuit/nanosuit.obj");

    // Lights
    glm::vec3 lightPos = glm::vec3(2.0, 4.0, -2.0);
    glm::vec3 lightColor = glm::vec3(0.2, 0.2, 0.7);

    // Set up G-Buffer
    // 3 textures:
    // 1. Positions + depth (RGBA)
    // 2. Color (RGB) 
    // 3. Normals (RGB) 
    GLuint gBuffer;
    glGenFramebuffers(1, &gBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    GLuint gPositionDepth, gNormal, gAlbedo;
    // - Position + linear depth color buffer
    glGenTextures(1, &gPositionDepth);
    glBindTexture(GL_TEXTURE_2D, gPositionDepth);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPositionDepth, 0);
    // - Normal color buffer
    glGenTextures(1, &gNormal);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
    // - Albedo color buffer
    glGenTextures(1, &gAlbedo);
    glBindTexture(GL_TEXTURE_2D, gAlbedo);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedo, 0);
    // - Tell OpenGL which color attachments we'll use (of this framebuffer) for rendering 
    GLuint attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
    glDrawBuffers(3, attachments);
    // - Create and attach depth buffer (renderbuffer)
    GLuint rboDepth;
    glGenRenderbuffers(1, &rboDepth);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    // - Finally check if framebuffer is complete
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "GBuffer Framebuffer not complete!" << std::endl;

    // Also create framebuffer to hold SSAO processing stage 
    GLuint ssaoFBO, ssaoBlurFBO;
    glGenFramebuffers(1, &ssaoFBO);  glGenFramebuffers(1, &ssaoBlurFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
    GLuint ssaoColorBuffer, ssaoColorBufferBlur;
    // - SSAO color buffer
    glGenTextures(1, &ssaoColorBuffer);
    glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBuffer, 0);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "SSAO Framebuffer not complete!" << std::endl;
    // - and blur stage
    glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
    glGenTextures(1, &ssaoColorBufferBlur);
    glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoColorBufferBlur, 0);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "SSAO Blur Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    // Sample kernel
    std::uniform_real_distribution<GLfloat> randomFloats(0.0, 1.0); // generates random floats between 0.0 and 1.0
    std::default_random_engine generator;
    std::vector<glm::vec3> ssaoKernel;
    for (GLuint i = 0; i < 64; ++i)
    {
        glm::vec3 sample(randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, randomFloats(generator));
        sample = glm::normalize(sample);
        sample *= randomFloats(generator);
        GLfloat scale = GLfloat(i) / 64.0;

        // Scale samples s.t. they're more aligned to center of kernel
        scale = lerp(0.1f, 1.0f, scale * scale);
        sample *= scale;
        ssaoKernel.push_back(sample);
    }

    // Noise texture
    std::vector<glm::vec3> ssaoNoise;
    for (GLuint i = 0; i < 16; i++)
    {
        glm::vec3 noise(randomFloats(generator) * 2.0 - 1.0, randomFloats(generator) * 2.0 - 1.0, 0.0f); // rotate around z-axis (in tangent space)
        ssaoNoise.push_back(noise);
    }
    GLuint noiseTexture; glGenTextures(1, &noiseTexture);
    glBindTexture(GL_TEXTURE_2D, noiseTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &ssaoNoise[0]);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);


    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Game loop
    while (!glfwWindowShouldClose(window))
    {
        // Set frame time
        GLfloat currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // Check and call events
        glfwPollEvents();
        Do_Movement();


        // 1. Geometry Pass: render scene's geometry/color data into gbuffer
        glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glm::mat4 projection = glm::perspective(camera.Zoom, (GLfloat)SCR_WIDTH / (GLfloat)SCR_HEIGHT, 0.1f, 50.0f);
            glm::mat4 view = camera.GetViewMatrix();
            glm::mat4 model;
            shaderGeometryPass.Use();
            glUniformMatrix4fv(glGetUniformLocation(shaderGeometryPass.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
            glUniformMatrix4fv(glGetUniformLocation(shaderGeometryPass.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));
            // Floor cube
            model = glm::translate(model, glm::vec3(0.0, -1.0f, 0.0f));
            model = glm::scale(model, glm::vec3(20.0f, 1.0f, 20.0f));
            glUniformMatrix4fv(glGetUniformLocation(shaderGeometryPass.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
            RenderCube();
            // Nanosuit model on the floor
            model = glm::mat4();
            model = glm::translate(model, glm::vec3(0.0f, 0.0f, 5.0));
            model = glm::rotate(model, -90.0f, glm::vec3(1.0, 0.0, 0.0));
            model = glm::scale(model, glm::vec3(0.5f));
            glUniformMatrix4fv(glGetUniformLocation(shaderGeometryPass.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
            nanosuit.Draw(shaderGeometryPass);
        glBindFramebuffer(GL_FRAMEBUFFER, 0);


        // 2. Create SSAO texture
        glBindFramebuffer(GL_FRAMEBUFFER, ssaoFBO);
            glClear(GL_COLOR_BUFFER_BIT);
            shaderSSAO.Use();
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, gPositionDepth);
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, gNormal);
            glActiveTexture(GL_TEXTURE2);
            glBindTexture(GL_TEXTURE_2D, noiseTexture);
            // Send kernel + rotation 
            for (GLuint i = 0; i < 64; ++i)
                glUniform3fv(glGetUniformLocation(shaderSSAO.Program, ("samples[" + std::to_string(i) + "]").c_str()), 1, &ssaoKernel[i][0]);
            glUniformMatrix4fv(glGetUniformLocation(shaderSSAO.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
            RenderQuad();
        glBindFramebuffer(GL_FRAMEBUFFER, 0);


        // 3. Blur SSAO texture to remove noise
        glBindFramebuffer(GL_FRAMEBUFFER, ssaoBlurFBO);
            glClear(GL_COLOR_BUFFER_BIT);
            shaderSSAOBlur.Use();
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, ssaoColorBuffer);
            RenderQuad();
        glBindFramebuffer(GL_FRAMEBUFFER, 0);


        // 4. Lighting Pass: traditional deferred Blinn-Phong lighting now with added screen-space ambient occlusion
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        shaderLightingPass.Use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, gPositionDepth);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, gNormal);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, gAlbedo);
        glActiveTexture(GL_TEXTURE3); // Add extra SSAO texture to lighting pass
        glBindTexture(GL_TEXTURE_2D, ssaoColorBufferBlur);
        // Also send light relevant uniforms
        glm::vec3 lightPosView = glm::vec3(camera.GetViewMatrix() * glm::vec4(lightPos, 1.0));
        glUniform3fv(glGetUniformLocation(shaderLightingPass.Program, "light.Position"), 1, &lightPosView[0]);
        glUniform3fv(glGetUniformLocation(shaderLightingPass.Program, "light.Color"), 1, &lightColor[0]);
        // Update attenuation parameters
        const GLfloat constant = 1.0; // Note that we don't send this to the shader, we assume it is always 1.0 (in our case)
        const GLfloat linear = 0.09;
        const GLfloat quadratic = 0.032;
        glUniform1f(glGetUniformLocation(shaderLightingPass.Program, "light.Linear"), linear);
        glUniform1f(glGetUniformLocation(shaderLightingPass.Program, "light.Quadratic"), quadratic);
        glUniform1i(glGetUniformLocation(shaderLightingPass.Program, "draw_mode"), draw_mode);
        RenderQuad();


        // Swap the buffers
        glfwSwapBuffers(window);
    }

    glfwTerminate();
    return 0;
}


// RenderQuad() Renders a 1x1 quad in NDC, best used for framebuffer color targets
// and post-processing effects.
GLuint quadVAO = 0;
GLuint quadVBO;
void RenderQuad()
{
    if (quadVAO == 0)
    {
        GLfloat quadVertices[] = {
            // Positions        // Texture Coords
            -1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
            -1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
            1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
            1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
        };
        // Setup plane VAO
        glGenVertexArrays(1, &quadVAO);
        glGenBuffers(1, &quadVBO);
        glBindVertexArray(quadVAO);
        glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    }
    glBindVertexArray(quadVAO);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);
}

// RenderCube() Renders a 1x1 3D cube in NDC.
GLuint cubeVAO = 0;
GLuint cubeVBO = 0;
void RenderCube()
{
    // Initialize (if necessary)
    if (cubeVAO == 0)
    {
        GLfloat vertices[] = {
            // Back face
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, // Bottom-left
            0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f, // top-right
            0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, // bottom-right         
            0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,  // top-right
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,  // bottom-left
            -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,// top-left
            // Front face
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom-left
            0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,  // bottom-right
            0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,  // top-right
            0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // top-right
            -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,  // top-left
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,  // bottom-left
            // Left face
            -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
            -0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-left
            -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-left
            -0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-left
            -0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,  // bottom-right
            -0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-right
            // Right face
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, // top-left
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom-right
            0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top-right         
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,  // bottom-right
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,  // top-left
            0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // bottom-left     
            // Bottom face
            -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
            0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f, // top-left
            0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,// bottom-left
            0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, // bottom-left
            -0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, // bottom-right
            -0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, // top-right
            // Top face
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
            0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
            0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, // top-right     
            0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom-right
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,// top-left
            -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f // bottom-left        
        };
        glGenVertexArrays(1, &cubeVAO);
        glGenBuffers(1, &cubeVBO);
        // Fill buffer
        glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        // Link vertex attributes
        glBindVertexArray(cubeVAO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    }
    // Render Cube
    glBindVertexArray(cubeVAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);
    glBindVertexArray(0);
}

bool keys[1024];
bool keysPressed[1024];
// Moves/alters the camera positions based on user input
void Do_Movement()
{
    // Camera controls
    if (keys[GLFW_KEY_W])
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (keys[GLFW_KEY_S])
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (keys[GLFW_KEY_A])
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (keys[GLFW_KEY_D])
        camera.ProcessKeyboard(RIGHT, deltaTime);

    if (keys[GLFW_KEY_1])
        draw_mode = 1;
    if (keys[GLFW_KEY_2])
        draw_mode = 2;
    if (keys[GLFW_KEY_3])
        draw_mode = 3;
    if (keys[GLFW_KEY_4])
        draw_mode = 4;
    if (keys[GLFW_KEY_5])
        draw_mode = 5;
}

GLfloat lastX = 400, lastY = 300;
bool firstMouse = true;
// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);

    if (key >= 0 && key <= 1024)
    {
        if (action == GLFW_PRESS)
            keys[key] = true;
        else if (action == GLFW_RELEASE)
        {
            keys[key] = false;
            keysPressed[key] = false;
        }
    }
}

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    GLfloat xoffset = xpos - lastX;
    GLfloat yoffset = lastY - ypos;

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

屏幕空間環境遮蔽是一個可高度自定義的效果,它的效果很大程度上依賴于我們根據場景類型調整它的參數。對所有類型的場景并不存在什么完美的參數組合方式。一些場景只在小半徑情況下工作,又有些場景會需要更大的半徑和更大的樣本數量才能看起來更真實。當前這個演示用了64個樣本,屬于比較多的了,你可以調調更小的核心大小從而獲得更好的結果。

一些你可以調整(比如說通過uniform)的參數:核心大小,半徑和/或噪聲核心的大小。你也可以提升最終的遮蔽值到一個用戶定義的冪從而增加它的強度:

occlusion = 1.0 - (occlusion / kernelSize);       
FragColor = pow(occlusion, power);

多試試不同的場景和不同的參數,來欣賞SSAO的可定制性。

盡管SSAO是一個很微小的效果,可能甚至不是很容易注意到,它在很大程度上增加了合適光照場景的真實性,它也絕對是一個在你工具箱中必備的技術。


附加資源

  • SSAO教程:John Chapman優秀的SSAO教程;本教程很大一部分代碼和技巧都是基于他的文章
  • 了解你的SSAO效果:關于提高SSAO特定效果的一篇很棒的文章
  • 深度值重構SSAO:OGLDev的一篇在SSAO之上的拓展教程,它討論了通過僅僅深度值重構位置矢量,節省了存儲開銷巨大的位置矢量到G緩沖的過程

后記

本篇已結束,下一篇關于PBR - 理論。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容