OpenGL 圖形庫的使用(三十五)—— 高級(jí)光照之陰影 - 點(diǎn)陰影Point Shadows

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.01.19

前言

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

點(diǎn)光源陰影

上個(gè)教程我們學(xué)到了如何使用陰影映射技術(shù)創(chuàng)建動(dòng)態(tài)陰影。效果不錯(cuò),但它只適合定向光,因?yàn)殛幱爸皇窃趩我欢ㄏ蚬庠聪律傻摹K运步卸ㄏ蜿幱坝成?,深度(陰影)貼圖生成自定向光的視角。

本節(jié)我們的焦點(diǎn)是在各種方向生成動(dòng)態(tài)陰影。這個(gè)技術(shù)可以適用于點(diǎn)光源,生成所有方向上的陰影。

這個(gè)技術(shù)叫做點(diǎn)光陰影,過去的名字是萬向陰影貼圖(omnidirectional shadow maps)技術(shù)。

本節(jié)代碼基于前面的陰影映射教程,所以如果你對(duì)傳統(tǒng)陰影映射不熟悉,還是建議先讀一讀陰影映射教程。 算法和定向陰影映射差不多:我們從光的透視圖生成一個(gè)深度貼圖,基于當(dāng)前fragment位置來對(duì)深度貼圖采樣,然后用儲(chǔ)存的深度值和每個(gè)fragment進(jìn)行對(duì)比,看看它是否在陰影中。定向陰影映射和萬向陰影映射的主要不同在于深度貼圖的使用上。

對(duì)于深度貼圖,我們需要從一個(gè)點(diǎn)光源的所有渲染場(chǎng)景,普通2D深度貼圖不能工作;如果我們使用立方體貼圖會(huì)怎樣?因?yàn)榱⒎襟w貼圖可以儲(chǔ)存6個(gè)面的環(huán)境數(shù)據(jù),它可以將整個(gè)場(chǎng)景渲染到立方體貼圖的每個(gè)面上,把它們當(dāng)作點(diǎn)光源四周的深度值來采樣。

生成后的深度立方體貼圖被傳遞到光照像素著色器,它會(huì)用一個(gè)方向向量來采樣立方體貼圖,從而得到當(dāng)前的fragment的深度(從光的透視圖)。大部分復(fù)雜的事情已經(jīng)在陰影映射教程中討論過了。算法只是在深度立方體貼圖生成上稍微復(fù)雜一點(diǎn)。


生成深度立方體貼圖

為創(chuàng)建一個(gè)光周圍的深度值的立方體貼圖,我們必須渲染場(chǎng)景6次:每次一個(gè)面。顯然渲染場(chǎng)景6次需要6個(gè)不同的視圖矩陣,每次把一個(gè)不同的立方體貼圖面附加到幀緩沖對(duì)象上。這看起來是這樣的:

for(int i = 0; i < 6; i++)
{
    GLuint face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
    BindViewMatrix(lightViewMatrices[i]);
    RenderScene();  
}

這會(huì)很耗費(fèi)性能因?yàn)橐粋€(gè)深度貼圖下需要進(jìn)行很多渲染調(diào)用。這個(gè)教程中我們將轉(zhuǎn)而使用另外的一個(gè)小技巧來做這件事,幾何著色器允許我們使用一次渲染過程來建立深度立方體貼圖。

首先,我們需要?jiǎng)?chuàng)建一個(gè)立方體貼圖:

GLuint depthCubemap;
glGenTextures(1, &depthCubemap);

然后生成立方體貼圖的每個(gè)面,將它們作為2D深度值紋理圖像:

const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
for (GLuint i = 0; i < 6; ++i)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, 
                     SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);

不要忘記設(shè)置合適的紋理參數(shù):

glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

正常情況下,我們把立方體貼圖紋理的一個(gè)面附加到幀緩沖對(duì)象上,渲染場(chǎng)景6次,每次將幀緩沖的深度緩沖目標(biāo)改成不同立方體貼圖面。由于我們將使用一個(gè)幾何著色器,它允許我們把所有面在一個(gè)過程渲染,我們可以使用glFramebufferTexture直接把立方體貼圖附加成幀緩沖的深度附件:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);

還要記得調(diào)用glDrawBufferglReadBuffer:當(dāng)生成一個(gè)深度立方體貼圖時(shí)我們只關(guān)心深度值,所以我們必須顯式告訴OpenGL這個(gè)幀緩沖對(duì)象不會(huì)渲染到一個(gè)顏色緩沖里。

萬向陰影貼圖有兩個(gè)渲染階段:首先我們生成深度貼圖,然后我們正常使用深度貼圖渲染,在場(chǎng)景中創(chuàng)建陰影。幀緩沖對(duì)象和立方體貼圖的處理看起是這樣的:

// 1. first render to depth cubemap
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glClear(GL_DEPTH_BUFFER_BIT);
    ConfigureShaderAndMatrices();
    RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. then render scene as normal with shadow mapping (using depth cubemap)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

這個(gè)過程和默認(rèn)的陰影映射一樣,盡管這次我們渲染和使用的是一個(gè)立方體貼圖深度紋理,而不是2D深度紋理。在我們實(shí)際開始從光的視角的所有方向渲染場(chǎng)景之前,我們先得計(jì)算出合適的變換矩陣。

1. 光空間的變換

設(shè)置了幀緩沖和立方體貼圖,我們需要一些方法來講場(chǎng)景的所有幾何體變換到6個(gè)光的方向中相應(yīng)的光空間。與陰影映射教程類似,我們將需要一個(gè)光空間的變換矩陣T,但是這次是每個(gè)面都有一個(gè)。

每個(gè)光空間的變換矩陣包含了投影和視圖矩陣。對(duì)于投影矩陣來說,我們將使用一個(gè)透視投影矩陣;光源代表一個(gè)空間中的點(diǎn),所以透視投影矩陣更有意義。每個(gè)光空間變換矩陣使用同樣的投影矩陣:

GLfloat aspect = (GLfloat)SHADOW_WIDTH/(GLfloat)SHADOW_HEIGHT;
GLfloat near = 1.0f;
GLfloat far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), aspect, near, far);

非常重要的一點(diǎn)是,這里glm::perspective的視野參數(shù),設(shè)置為90度。90度我們才能保證視野足夠大到可以合適地填滿立方體貼圖的一個(gè)面,立方體貼圖的所有面都能與其他面在邊緣對(duì)齊。

因?yàn)橥队熬仃囋诿總€(gè)方向上并不會(huì)改變,我們可以在6個(gè)變換矩陣中重復(fù)使用。我們要為每個(gè)方向提供一個(gè)不同的視圖矩陣。用glm::lookAt創(chuàng)建6個(gè)觀察方向,每個(gè)都按順序注視著立方體貼圖的的一個(gè)方向:右、左、上、下、近、遠(yuǎn):

std::vector<glm::mat4> shadowTransforms;
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(1.0,0.0,0.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(-1.0,0.0,0.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(0.0,1.0,0.0), glm::vec3(0.0,0.0,1.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(0.0,-1.0,0.0), glm::vec3(0.0,0.0,-1.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(0.0,0.0,1.0), glm::vec3(0.0,-1.0,0.0));
shadowTransforms.push_back(shadowProj * 
                 glm::lookAt(lightPos, lightPos + glm::vec3(0.0,0.0,-1.0), glm::vec3(0.0,-1.0,0.0));

這里我們創(chuàng)建了6個(gè)視圖矩陣,把它們乘以投影矩陣,來得到6個(gè)不同的光空間變換矩陣。glm::lookAt的target參數(shù)是它注視的立方體貼圖的面的一個(gè)方向。

這些變換矩陣發(fā)送到著色器渲染到立方體貼圖里。

2. 深度著色器

為了把值渲染到深度立方體貼圖,我們將需要3個(gè)著色器:頂點(diǎn)和像素著色器,以及一個(gè)它們之間的幾何著色器。

幾何著色器是負(fù)責(zé)將所有世界空間的頂點(diǎn)變換到6個(gè)不同的光空間的著色器。因此頂點(diǎn)著色器簡(jiǎn)單地將頂點(diǎn)變換到世界空間,然后直接發(fā)送到幾何著色器:

#version 330 core
layout (location = 0) in vec3 position;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(position, 1.0);
}

緊接著幾何著色器以3個(gè)三角形的頂點(diǎn)作為輸入,它還有一個(gè)光空間變換矩陣的uniform數(shù)組。幾何著色器接下來會(huì)負(fù)責(zé)將頂點(diǎn)變換到光空間;這里它開始變得有趣了。

幾何著色器有一個(gè)內(nèi)建變量叫做gl_Layer,它指定發(fā)散出基本圖形送到立方體貼圖的哪個(gè)面。當(dāng)不管它時(shí),幾何著色器就會(huì)像往常一樣把它的基本圖形發(fā)送到輸送管道的下一階段,但當(dāng)我們更新這個(gè)變量就能控制每個(gè)基本圖形將渲染到立方體貼圖的哪一個(gè)面。當(dāng)然這只有當(dāng)我們有了一個(gè)附加到激活的幀緩沖的立方體貼圖紋理才有效:

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;

uniform mat4 shadowMatrices[6];

out vec4 FragPos; // FragPos from GS (output per emitvertex)

void main()
{
    for(int face = 0; face < 6; ++face)
    {
        gl_Layer = face; // built-in variable that specifies to which face we render.
        for(int i = 0; i < 3; ++i) // for each triangle's vertices
        {
            FragPos = gl_in[i].gl_Position;
            gl_Position = shadowMatrices[face] * FragPos;
            EmitVertex();
        }    
        EndPrimitive();
    }
}

幾何著色器相對(duì)簡(jiǎn)單。我們輸入一個(gè)三角形,輸出總共6個(gè)三角形(6*3頂點(diǎn),所以總共18個(gè)頂點(diǎn))。在main函數(shù)中,我們遍歷立方體貼圖的6個(gè)面,我們每個(gè)面指定為一個(gè)輸出面,把這個(gè)面的interger(整數(shù))存到gl_Layer。然后,我們通過把面的光空間變換矩陣乘以FragPos,將每個(gè)世界空間頂點(diǎn)變換到相關(guān)的光空間,生成每個(gè)三角形。注意,我們還要將最后的FragPos變量發(fā)送給像素著色器,我們需要計(jì)算一個(gè)深度值。

上個(gè)教程,我們使用的是一個(gè)空的像素著色器,讓OpenGL配置深度貼圖的深度值。這次我們將計(jì)算自己的深度,這個(gè)深度就是每個(gè)fragment位置和光源位置之間的線性距離。計(jì)算自己的深度值使得之后的陰影計(jì)算更加直觀。

#version 330 core
in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()
{
    // get distance between fragment and light source
    float lightDistance = length(FragPos.xyz - lightPos);

    // map to [0;1] range by dividing by far_plane
    lightDistance = lightDistance / far_plane;

    // Write this as modified depth
    gl_FragDepth = lightDistance;
}

像素著色器將來自幾何著色器的FragPos、光的位置向量和視錐的遠(yuǎn)平面值作為輸入。這里我們把fragment和光源之間的距離,映射到0到1的范圍,把它寫入為fragment的深度值。

使用這些著色器渲染場(chǎng)景,立方體貼圖附加的幀緩沖對(duì)象激活以后,你會(huì)得到一個(gè)完全填充的深度立方體貼圖,以便于進(jìn)行第二階段的陰影計(jì)算。


萬向陰影貼圖

所有事情都做好了,是時(shí)候來渲染萬向陰影(Omnidirectional Shadow)了。這個(gè)過程和定向陰影映射教程相似,盡管這次我們綁定的深度貼圖是一個(gè)立方體貼圖,而不是2D紋理,并且將光的投影的遠(yuǎn)平面發(fā)送給了著色器。

glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();  
// ... send uniforms to shader (including light's far_plane value)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... bind other textures
RenderScene();

這里的renderScene函數(shù)在一個(gè)大立方體房間中渲染一些立方體,它們散落在大立方體各處,光源在場(chǎng)景中央。

頂點(diǎn)著色器和像素著色器和原來的陰影映射著色器大部分都一樣:不同之處是在光空間中像素著色器不再需要一個(gè)fragment位置,現(xiàn)在我們可以使用一個(gè)方向向量采樣深度值。

因?yàn)檫@個(gè)頂點(diǎn)著色器不再需要將他的位置向量變換到光空間,所以我們可以去掉FragPosLightSpace變量:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 texCoords;

out vec2 TexCoords;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    vs_out.FragPos = vec3(model * vec4(position, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * normal;
    vs_out.TexCoords = texCoords;
}

片段著色器的Blinn-Phong光照代碼和我們之前陰影相乘的結(jié)尾部分一樣:

#version 330 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

uniform float far_plane;

float ShadowCalculation(vec3 fragPos)
{
    [...]
}

void main()
{           
    vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
    vec3 normal = normalize(fs_in.Normal);
    vec3 lightColor = vec3(0.3);
    // Ambient
    vec3 ambient = 0.3 * color;
    // Diffuse
    vec3 lightDir = normalize(lightPos - fs_in.FragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * lightColor;
    // Specular
    vec3 viewDir = normalize(viewPos - fs_in.FragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = 0.0;
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * lightColor;    
    // Calculate shadow
    float shadow = ShadowCalculation(fs_in.FragPos);                      
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    

    FragColor = vec4(lighting, 1.0f);
}

有一些細(xì)微的不同:光照代碼一樣,但我們現(xiàn)在有了一個(gè)uniform變量samplerCube,shadowCalculation函數(shù)用fragment的位置作為它的參數(shù),取代了光空間的fragment位置。我們現(xiàn)在還要引入光的視錐的遠(yuǎn)平面值,后面我們會(huì)需要它。像素著色器的最后,我們計(jì)算出陰影元素,當(dāng)fragment在陰影中時(shí)它是1.0,不在陰影中時(shí)是0.0。我們使用計(jì)算出來的陰影元素去影響光照的diffuse和specular元素。

ShadowCalculation函數(shù)中有很多不同之處,現(xiàn)在是從立方體貼圖中進(jìn)行采樣,不再使用2D紋理了。我們來一步一步的討論一下的它的內(nèi)容。

我們需要做的第一件事是獲取立方體貼圖的森都。你可能已經(jīng)從教程的立方體貼圖部分想到,我們已經(jīng)將深度儲(chǔ)存為fragment和光位置之間的距離了;我們這里采用相似的處理方式:

float ShadowCalculation(vec3 fragPos)
{
    vec3 fragToLight = fragPos - lightPos; 
    float closestDepth = texture(depthMap, fragToLight).r;
}

在這里,我們得到了fragment的位置與光的位置之間的不同的向量,使用這個(gè)向量作為一個(gè)方向向量去對(duì)立方體貼圖進(jìn)行采樣。方向向量不需要是單位向量,所以無需對(duì)它進(jìn)行標(biāo)準(zhǔn)化。最后的closestDepth是光源和它最接近的可見fragment之間的標(biāo)準(zhǔn)化的深度值。

closestDepth值現(xiàn)在在0到1的范圍內(nèi)了,所以我們先將其轉(zhuǎn)換會(huì)0到far_plane的范圍,這需要把他乘以far_plane:

closestDepth *= far_plane;

下一步我們獲取當(dāng)前fragment和光源之間的深度值,我們可以簡(jiǎn)單的使用fragToLight的長(zhǎng)度來獲取它,這取決于我們?nèi)绾斡?jì)算立方體貼圖中的深度值:

float currentDepth = length(fragToLight);

返回的是和closestDepth范圍相同的深度值。

現(xiàn)在我們可以將兩個(gè)深度值對(duì)比一下,看看哪一個(gè)更接近,以此決定當(dāng)前的fragment是否在陰影當(dāng)中。我們還要包含一個(gè)陰影偏移,所以才能避免陰影失真,這在前面教程中已經(jīng)討論過了。

float bias = 0.05; 
float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;

完整的ShadowCalculation現(xiàn)在變成了這樣:

float ShadowCalculation(vec3 fragPos)
{
    // Get vector between fragment position and light position
    vec3 fragToLight = fragPos - lightPos;
    // Use the light to fragment vector to sample from the depth map    
    float closestDepth = texture(depthMap, fragToLight).r;
    // It is currently in linear range between [0,1]. Re-transform back to original value
    closestDepth *= far_plane;
    // Now get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);
    // Now test for shadows
    float bias = 0.05; 
    float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;

    return shadow;
}

有了這些著色器,我們已經(jīng)能得到非常好的陰影效果了,這次從一個(gè)點(diǎn)光源所有周圍方向上都有陰影。有一個(gè)位于場(chǎng)景中心的點(diǎn)光源,看起來會(huì)像這樣:

你可以從這里找到這個(gè)demo的源碼、頂點(diǎn)片段著色器。

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

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

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

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

// Other Libs
#include <SOIL.h>

// 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();
GLuint loadTexture(GLchar* path);
void RenderScene(Shader &shader);
void RenderCube();
void RenderQuad();

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

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

// Options
GLboolean shadows = true;

// Global variables
GLuint woodTexture;
GLuint planeVAO;

// 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();

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

    // Setup some OpenGL options
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);

    // Setup and compile our shaders
    Shader shader("point_shadows.vs", "point_shadows.frag");
    Shader simpleDepthShader("point_shadows_depth.vs", "point_shadows_depth.frag", "point_shadows_depth.gs");

    // Set texture samples
    shader.Use();
    glUniform1i(glGetUniformLocation(shader.Program, "diffuseTexture"), 0);
    glUniform1i(glGetUniformLocation(shader.Program, "depthMap"), 1);

    // Light source
    glm::vec3 lightPos(0.0f, 0.0f, 0.0f);

    // Load textures
    woodTexture = loadTexture("../../../resources/textures/wood.png");

    // Configure depth map FBO
    const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
    GLuint depthMapFBO;
    glGenFramebuffers(1, &depthMapFBO);
    // Create depth cubemap texture
    GLuint depthCubemap;
    glGenTextures(1, &depthCubemap);
    glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
    for (GLuint i = 0; i < 6; ++i)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    // Attach cubemap as depth map FBO's color buffer
    glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    glClearColor(0.1f, 0.1f, 0.1f, 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();

        // Move light position over time
        //lightPos.z = sin(glfwGetTime() * 0.5) * 3.0;

        // 0. Create depth cubemap transformation matrices
        GLfloat aspect = (GLfloat)SHADOW_WIDTH / (GLfloat)SHADOW_HEIGHT;
        GLfloat near = 1.0f;
        GLfloat far = 25.0f;
        glm::mat4 shadowProj = glm::perspective(90.0f, aspect, near, far);
        std::vector<glm::mat4> shadowTransforms;
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3( 1.0,  0.0,  0.0), glm::vec3(0.0, -1.0,  0.0)));
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3(-1.0,  0.0,  0.0), glm::vec3(0.0, -1.0,  0.0)));
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3( 0.0,  1.0,  0.0), glm::vec3(0.0,  0.0,  1.0)));
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3( 0.0, -1.0,  0.0), glm::vec3(0.0,  0.0, -1.0)));
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3( 0.0,  0.0,  1.0), glm::vec3(0.0, -1.0,  0.0)));
        shadowTransforms.push_back(shadowProj * glm::lookAt(lightPos, lightPos + glm::vec3( 0.0,  0.0, -1.0), glm::vec3(0.0, -1.0,  0.0)));

        // 1. Render scene to depth cubemap
        glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
        glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
            glClear(GL_DEPTH_BUFFER_BIT);
            simpleDepthShader.Use();
            for (GLuint i = 0; i < 6; ++i)
                glUniformMatrix4fv(glGetUniformLocation(simpleDepthShader.Program, ("shadowMatrices[" + std::to_string(i) + "]").c_str()), 1, GL_FALSE, glm::value_ptr(shadowTransforms[i]));
            glUniform1f(glGetUniformLocation(simpleDepthShader.Program, "far_plane"), far);
            glUniform3fv(glGetUniformLocation(simpleDepthShader.Program, "lightPos"), 1, &lightPos[0]);
            RenderScene(simpleDepthShader);
         glBindFramebuffer(GL_FRAMEBUFFER, 0);

        // 2. Render scene as normal 
        glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        shader.Use();
        glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
        glm::mat4 view = camera.GetViewMatrix();
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));
        // Set light uniforms
        glUniform3fv(glGetUniformLocation(shader.Program, "lightPos"), 1, &lightPos[0]);
        glUniform3fv(glGetUniformLocation(shader.Program, "viewPos"), 1, &camera.Position[0]);
        // Enable/Disable shadows by pressing 'SPACE'
        glUniform1i(glGetUniformLocation(shader.Program, "shadows"), shadows);
        glUniform1f(glGetUniformLocation(shader.Program, "far_plane"), far);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, woodTexture);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
        RenderScene(shader);

        // Swap the buffers
        glfwSwapBuffers(window);
    }

    glfwTerminate();
    return 0;
}

void RenderScene(Shader &shader)
{
    // Room cube
    glm::mat4 model;
    model = glm::scale(model, glm::vec3(10.0));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    glDisable(GL_CULL_FACE); // Note that we disable culling here since we render 'inside' the cube instead of the usual 'outside' which throws off the normal culling methods.
    glUniform1i(glGetUniformLocation(shader.Program, "reverse_normals"), 1); // A small little hack to invert normals when drawing cube from the inside so lighting still works.
    RenderCube();
    glUniform1i(glGetUniformLocation(shader.Program, "reverse_normals"), 0); // And of course disable it
    glEnable(GL_CULL_FACE);
    // Cubes
    model = glm::mat4();
    model = glm::translate(model, glm::vec3(4.0f, -3.5f, 0.0));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    RenderCube();
    model = glm::mat4();
    model = glm::translate(model, glm::vec3(2.0f, 3.0f, 1.0));
    model = glm::scale(model, glm::vec3(1.5));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    RenderCube();
    model = glm::mat4();
    model = glm::translate(model, glm::vec3(-3.0f, -1.0f, 0.0));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    RenderCube();
    model = glm::mat4();
    model = glm::translate(model, glm::vec3(-1.5f, 1.0f, 1.5));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    RenderCube();
    model = glm::mat4();
    model = glm::translate(model, glm::vec3(-1.5f, 2.0f, -3.0));
    model = glm::rotate(model, 60.0f, glm::normalize(glm::vec3(1.0, 0.0, 1.0)));
    model = glm::scale(model, glm::vec3(1.5));
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));
    RenderCube();
}


// 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);
}

// This function loads a texture from file. Note: texture loading functions like these are usually 
// managed by a 'Resource Manager' that manages all resources (like textures, models, audio). 
// For learning purposes we'll just define it as a utility function.
GLuint loadTexture(GLchar* path)
{
    // Generate texture ID and load texture data 
    GLuint textureID;
    glGenTextures(1, &textureID);
    int width, height;
    unsigned char* image = SOIL_load_image(path, &width, &height, 0, SOIL_LOAD_RGB);
    // Assign texture to ID
    glBindTexture(GL_TEXTURE_2D, textureID);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glGenerateMipmap(GL_TEXTURE_2D);

    // Parameters
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);
    SOIL_free_image_data(image);
    return textureID;

}

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_SPACE] && !keysPressed[GLFW_KEY_SPACE])
    {
        shadows = !shadows;
        keysPressed[GLFW_KEY_SPACE] = true;
    }
}

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);
}

1. 顯示立方體貼圖深度緩沖

如果你想我一樣第一次并沒有做對(duì),那么就要進(jìn)行調(diào)試排錯(cuò),將深度貼圖顯示出來以檢查其是否正確。因?yàn)槲覀儾辉儆?D深度貼圖紋理,深度貼圖的顯示不會(huì)那么顯而易見。

一個(gè)簡(jiǎn)單的把深度緩沖顯示出來的技巧是,在ShadowCalculation函數(shù)中計(jì)算標(biāo)準(zhǔn)化的closestDepth變量,把變量顯示為:

FragColor = vec4(vec3(closestDepth / far_plane), 1.0);

結(jié)果是一個(gè)灰度場(chǎng)景,每個(gè)顏色代表著場(chǎng)景的線性深度值:

你可能也注意到了帶陰影部分在墻外。如果看起來和這個(gè)差不多,你就知道深度立方體貼圖生成的沒錯(cuò)。否則你可能做錯(cuò)了什么,也許是closestDepth仍然還在0到far_plane的范圍。


PCF

由于萬向陰影貼圖基于傳統(tǒng)陰影映射的原則,它便也繼承了由解析度產(chǎn)生的非真實(shí)感。如果你放大就會(huì)看到鋸齒邊了。PCF或稱Percentage-closer filtering允許我們通過對(duì)fragment位置周圍過濾多個(gè)樣本,并對(duì)結(jié)果平均化。

如果我們用和前面教程同樣的那個(gè)簡(jiǎn)單的PCF過濾器,并加入第三個(gè)維度,就是這樣的:

float shadow = 0.0;
float bias = 0.05; 
float samples = 4.0;
float offset = 0.1;
for(float x = -offset; x < offset; x += offset / (samples * 0.5))
{
    for(float y = -offset; y < offset; y += offset / (samples * 0.5))
    {
        for(float z = -offset; z < offset; z += offset / (samples * 0.5))
        {
            float closestDepth = texture(depthMap, fragToLight + vec3(x, y, z)).r; 
            closestDepth *= far_plane;   // Undo mapping [0;1]
            if(currentDepth - bias > closestDepth)
                shadow += 1.0;
        }
    }
}
shadow /= (samples * samples * samples);

這段代碼和我們傳統(tǒng)的陰影映射沒有多少不同。這里我們根據(jù)樣本的數(shù)量動(dòng)態(tài)計(jì)算了紋理偏移量,我們?cè)谌齻€(gè)軸向采樣三次,最后對(duì)子樣本進(jìn)行平均化。

現(xiàn)在陰影看起來更加柔和平滑了,由此得到更加真實(shí)的效果:

然而,samples設(shè)置為4.0,每個(gè)fragment我們會(huì)得到總共64個(gè)樣本,這太多了!

大多數(shù)這些樣本都是多余的,它們?cè)谠挤较蛳蛄拷幉蓸?,不如在采樣方向向量的垂直方向進(jìn)行采樣更有意義。可是,沒有(簡(jiǎn)單的)方式能夠指出哪一個(gè)子方向是多余的,這就難了。有個(gè)技巧可以使用,用一個(gè)偏移量方向數(shù)組,它們差不多都是分開的,每一個(gè)指向完全不同的方向,剔除彼此接近的那些子方向。下面就是一個(gè)有著20個(gè)偏移方向的數(shù)組:

vec3 sampleOffsetDirections[20] = vec3[]
(
   vec3( 1,  1,  1), vec3( 1, -1,  1), vec3(-1, -1,  1), vec3(-1,  1,  1), 
   vec3( 1,  1, -1), vec3( 1, -1, -1), vec3(-1, -1, -1), vec3(-1,  1, -1),
   vec3( 1,  1,  0), vec3( 1, -1,  0), vec3(-1, -1,  0), vec3(-1,  1,  0),
   vec3( 1,  0,  1), vec3(-1,  0,  1), vec3( 1,  0, -1), vec3(-1,  0, -1),
   vec3( 0,  1,  1), vec3( 0, -1,  1), vec3( 0, -1, -1), vec3( 0,  1, -1)
);

然后我們把PCF算法與從sampleOffsetDirections得到的樣本數(shù)量進(jìn)行適配,使用它們從立方體貼圖里采樣。這么做的好處是與之前的PCF算法相比,我們需要的樣本數(shù)量變少了。

float shadow = 0.0;
float bias = 0.15;
int samples = 20;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0; i < samples; ++i)
{
    float closestDepth = texture(depthMap, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
    closestDepth *= far_plane;   // Undo mapping [0;1]
    if(currentDepth - bias > closestDepth)
        shadow += 1.0;
}
shadow /= float(samples);

這里我們把一個(gè)偏移量添加到指定的diskRadius中,它在fragToLight方向向量周圍從立方體貼圖里采樣。

另一個(gè)在這里可以應(yīng)用的有意思的技巧是,我們可以基于觀察者里一個(gè)fragment的距離來改變diskRadius;這樣我們就能根據(jù)觀察者的距離來增加偏移半徑了,當(dāng)距離更遠(yuǎn)的時(shí)候陰影更柔和,更近了就更銳利。

float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;

PCF算法的結(jié)果如果沒有變得更好,也是非常不錯(cuò)的,這是柔和的陰影效果:

當(dāng)然了,我們添加到每個(gè)樣本的bias(偏移)高度依賴于上下文,總是要根據(jù)場(chǎng)景進(jìn)行微調(diào)的。試試這些值,看看怎樣影響了場(chǎng)景。 這里是最終版本的頂點(diǎn)和像素著色器。

我還要提醒一下使用幾何著色器來生成深度貼圖不會(huì)一定比每個(gè)面渲染場(chǎng)景6次更快。使用幾何著色器有它自己的性能局限,在第一個(gè)階段使用它可能獲得更好的性能表現(xiàn)。這取決于環(huán)境的類型,以及特定的顯卡驅(qū)動(dòng)等等,所以如果你很關(guān)心性能,就要確保對(duì)兩種方法有大致了解,然后選擇對(duì)你場(chǎng)景來說更高效的那個(gè)。我個(gè)人還是喜歡使用幾何著色器來進(jìn)行陰影映射,原因很簡(jiǎn)單,因?yàn)樗鼈兪褂闷饋砀?jiǎn)單。


附加資源

后記

本篇已結(jié)束,下一篇關(guān)于高級(jí)光照的法線貼圖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容