點(diǎn)陰影(point shadow)
上一章節(jié)我們了解使用陰影映射創(chuàng)建動(dòng)態(tài)陰影,但是只適合用于定向光源產(chǎn)生的陰影璃诀,因此也稱(chēng)為定向陰影映射(directional shadow mapping)逢并。本章我們討論如何在所有方向上生成動(dòng)態(tài)陰影翠胰,這項(xiàng)技術(shù)特別適合點(diǎn)光源邓尤,因此也稱(chēng)為點(diǎn)陰影(point shadow)畔况,或更正式的名稱(chēng)叫做全向陰影映射(omnidirectional shadow mapping)敦跌。
- 全向陰影映射與定向陰影映射相似背伴,都是先生成基于光源視角的深度圖,然后基于片元位置從深度圖采樣峰髓,最后通過(guò)比較每個(gè)片元當(dāng)前存儲(chǔ)的深度值來(lái)判斷是否處于陰影中傻寂,兩者的主要區(qū)別就是所使用的深度圖。
- 全向陰影映射使用立方體貼圖將整個(gè)場(chǎng)景渲染到立方體的各個(gè)面携兵,并從這6個(gè)面中采樣點(diǎn)光源周?chē)h(huán)境的深度值疾掰。見(jiàn)下圖:(圖片取自書(shū)中)
1. 生成深度立方體貼圖
- 創(chuàng)建一個(gè)環(huán)繞光源的深度立方體貼圖的一種方式就是使用6個(gè)視矩陣分別渲染場(chǎng)景6次,每次將立方體貼圖的不同面附加到幀緩沖區(qū)徐紧。代碼看起來(lái)如下:
for (unsigned int i = 0; i < 6; i++)
{
GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
BindViewMatrix(lightViewMatrices[i]);
RenderScene();
}
- 使用上述方法需要很多渲染操作調(diào)用静檬,太過(guò)繁瑣,本章我們采用另外一種方式:在幾何著色器中使用一個(gè)小技巧來(lái)讓我們用一次渲染調(diào)用完成立方體貼圖的構(gòu)建并级。(注意:采用幾何著色器的方式不一定性能更好拂檩,具體哪種方法性能更優(yōu)需根據(jù)渲染的場(chǎng)景,顯卡型號(hào)等進(jìn)行測(cè)試)
- 首先生成立方體貼圖嘲碧。
unsigned int depthCubemap;
glGenTextures(1, &depthCubemap);
- 為每個(gè)立方體貼圖面指定一個(gè)2D深度值紋理圖像稻励。
const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
for (unsigned int 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_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
- 使用
glFramebufferTexture
函數(shù)將立方體貼圖紋理附加為幀緩沖區(qū)的深度附件。
- 使用
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- 與上一章節(jié)相似望抽,陰影映射的兩個(gè)階段的偽代碼如下:
// 第一階段:渲染深度立方體貼圖
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 第二階段:使用深度立方體貼圖渲染場(chǎng)景
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();
1.1 基于光源視角轉(zhuǎn)換
- 設(shè)置好幀緩沖區(qū)和立方體貼圖后加矛,我們需要一種方法將場(chǎng)景的所有幾何基元轉(zhuǎn)換到光源6個(gè)方向上的光源空間。與陰影映射章節(jié)一樣煤篙,我們需要一個(gè)光源空間的轉(zhuǎn)換矩陣斟览,但是這一次立方體的每個(gè)面都需要一個(gè)。
- 光源空間轉(zhuǎn)換矩陣包含一個(gè)投影矩陣和一個(gè)視矩陣辑奈,對(duì)于每個(gè)轉(zhuǎn)換矩陣我們使用相同的投影矩陣苛茂。
float aspect = (float)SHADOW_WIDTH / (float)SHADOW_HEIGHT;
float near = 1.0f;
float far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), apsect, near, far);
- 對(duì)于投影矩陣需要注意的一點(diǎn)是我們將視角角度設(shè)置為90.0f。這是為了保證視場(chǎng)正好大到足夠填充立方體貼圖的一個(gè)面鸠窗,這樣所有的面就能夠沿著邊緣對(duì)齊味悄。
- 每個(gè)方向我們使用相同的投影矩陣,但是對(duì)于視矩陣塌鸯,我們需要使用
glm::lookAt
函數(shù)創(chuàng)建面向立方體貼圖6個(gè)面的6個(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)));
1.2 深度著色器
- 要渲染深度值到立方體貼圖庭瑰,我們需要完整使用三種著色器。其中幾何著色器負(fù)責(zé)將頂點(diǎn)坐標(biāo)從世界空間轉(zhuǎn)換到6個(gè)不同的光源空間抢埋。因此弹灭,頂點(diǎn)著色器只是將頂點(diǎn)坐標(biāo)轉(zhuǎn)換到世界空間并傳遞給幾何著色器。頂點(diǎn)著色器如下:
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(aPos, 1.0);
}
- 幾何著色器使用內(nèi)置變量
gl_Layer
來(lái)指定往立方體貼圖的那個(gè)面輸出基元揪垄。如果不管該變量穷吮,幾何著色器像往常一樣將數(shù)據(jù)傳遞到渲染管道的下一個(gè)階段,但是如果我們更新該變量我們可以控制將每個(gè)基元渲染到立方體貼圖的那個(gè)面饥努。當(dāng)然這需要有一個(gè)立方體貼圖紋理附加到當(dāng)前激活的幀緩沖區(qū)捡鱼。幾何著色器如下:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 18) out;
uniform mat4 shadowMatrices[6];
out vec4 FragPos;
void main()
{
for (int face = 0; face < 6; ++face)
{
gl_Layer = face; // 指定渲染到那個(gè)面
for (int i = 0; i < 3; ++i) // 每個(gè)三角形頂點(diǎn)
{
FragPos = gl_in[i].gl_Position;
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
- 上一章我們使用一個(gè)空的片元著色器,讓OpenGL自己決定深度圖的深度值酷愧。這次我們自己計(jì)算最近片元位置與光源位置的線性距離作為深度值驾诈。片元著色器如下:
#version 330 core
in vec4 FragPos;
uniform vec3 lightPos;
uniform float far_plane;
void mian()
{
// 獲取片元與光源的距離
float lightDiatance = length(FragPos.xyz, lightPos);
// 除以far_plane,映射到[0;1]范圍
lightDiatance = lightDiatance / far_plane;
// 寫(xiě)入深度值
gl_FragDepth = lightDiatance;
}
2. 全向陰影映射
- 渲染全向陰影的過(guò)程與定向陰影映射相似溶浴,只是這次我們需要綁定立方體貼圖紋理并且將光源投影的遠(yuǎn)平面變量傳遞給著色器乍迄。偽代碼如下:
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
// ... 發(fā)送變量值到著色器
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// 綁定其他紋理
RenderScene();
- 場(chǎng)景的頂點(diǎn)著色器和片元著色器與陰影映射章節(jié)的相似,差別在于我們現(xiàn)在使用方向矢量來(lái)采樣深度值士败,因此不需要光源空間的片元位置闯两。因此我們可以移除頂點(diǎn)著色器的
FragPosLightSpace
變量。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out VS_OUT
{
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;
void main()
{
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
vs_out.TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
- 片元著色器主要改變?cè)谟陉幱坝?jì)算函數(shù),因?yàn)楝F(xiàn)在我們需要從立方體貼圖紋理而不是二維紋理采樣深度值生蚁。下面我們逐步討論函數(shù)的內(nèi)容。首先我們需要從立方體貼圖紋理檢索深度值戏自。
float ShadowCaculation(float fragPos)
{
vec3 fragToLight = fragPos - lightPos;
float closestDepth = texture(depthMap, fragToLight).r;
}
- 將深度值從[0, 1]轉(zhuǎn)換到[0, far_plane]邦投。
closestDepth *= far_plane;
- 檢索當(dāng)前片元的深度值,由前面我們計(jì)算深度值的方式擅笔,我們知道其實(shí)就是片元與光源之間的距離志衣。
float currentDepth = length(fragToLight);
- 計(jì)算陰影值并應(yīng)用偏移消除陰影粉刺。
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
- 最后猛们,完整的片元著色器如下:
#version 330 core
out vec4 FragColor;
in VS_OUT
{
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} fs_in;
uniform sampler2D diffuseTexture;
uniform samplerCube shadowMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
float ShadowCaculation(vec3 fragPos)
{
vec3 fragToLight = fragPos - lightPos;
float closestDepth = texture(shadowMap, fragToLight).r;
closestDepth *= far_plane;
float currentDepth = length(fragToLight);
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
return shadow;
}
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);
float spec = 0.0;
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
vec3 specular = spec * lightColor;
// caculate shadow
float shadow = ShadowCalculation(fs_in.FragPos);
vec3 lighting = (ambient + (1.0 -shadow) * (diffuse + specular)) * color;
FragColor = vec4(lighting, 1.0);
}
-
渲染效果念脯。
- 當(dāng)程序渲染異常時(shí),一般我們都會(huì)檢查深度圖是否正常構(gòu)建弯淘÷痰辏可視化深度緩沖區(qū)我們可以采用
ShadowCaculation
函數(shù)中的closestDepth
作為片元輸出。
vec3 fragToLight = fs_in.FragPos - lightPos;
float closestDepth = texture(shadowMap, fragToLight).r;
FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
-
深度立方體貼圖庐橙。
3. PCF
- 全向陰影映射與定向陰影映射都基于相同的準(zhǔn)則假勿,因此都存在依賴(lài)于分辨率的偽影(見(jiàn)上圖)。我們可以采取與上一章相同的PCF過(guò)濾器來(lái)平滑邊緣鋸齒态鳖。在上一章PCF的基礎(chǔ)上我們添加第三個(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;
if(currentDepth - bias > closestDepth)
shadow += 1.0;
}
}
}
shadow /= (samples * samples * samples);
-
渲染效果如下:
- 上述PCF使用四個(gè)采樣點(diǎn),這樣每個(gè)片元需要進(jìn)行64次采樣浆竭,增加了很多計(jì)算浸须。而且這些采樣很多都是冗余的,因?yàn)檫@里面很多與原來(lái)采樣的方向矢量十分接近邦泄。但是我們也很難區(qū)分哪些子采樣是冗余的删窒,有一個(gè)小技巧就是我們使用一個(gè)偏移數(shù)組來(lái)區(qū)分采樣方向矢量,讓不同子采樣指向不同的方向顺囊。這樣我們就可以降低子采樣的數(shù)量易稠。下面是一個(gè)20個(gè)元素的偏移數(shù)組:
vec3 samplesOffsetDirections[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)
);
- 使用上面的偏移數(shù)組,我們可以調(diào)整PCF算法包蓝,采用固定數(shù)量的子采樣來(lái)對(duì)立方體貼圖進(jìn)行采樣驶社。
float shadow = 0.0;
float bias = 0.05;
int samples = 20.0;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0;i < 20; ++i)
{
float closestDepth = texture(depthMap, fragToLight + samplesOffsetDirections[i] * diskRadius).r;
closestDepth *= far_plane;
if(currentDepth - bias > closestDepth)
shadow += 1.0;
}
shadow /= float(samples);
- 另外一個(gè)技巧是我們可以根據(jù)觀察者與片元的距離調(diào)整
diskRadius
的大小,這樣可以讓視角拉遠(yuǎn)時(shí)陰影更柔和测萎,拉近時(shí)則更銳化亡电。
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
-
渲染效果。