1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-24 05:05:29 +08:00

Revert all the changes since 851f9ec

This commit is contained in:
Meow J
2016-07-04 19:21:32 +08:00
parent d8ebc4ee4f
commit 12a8b3eb38
144 changed files with 134 additions and 166 deletions

View File

@@ -0,0 +1,558 @@
## 阴影映射(Shadow Mapping)
本文作者JoeyDeVries由Django翻译自[http://learnopengl.com](http://learnopengl.com)
原文 | [Shadow Mapping](http://learnopengl.com/#!Advanced-Lighting/Shadows/Shadow-Mapping)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | gjy_1992
阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。场景和物体的深度感因此能够得到极大提升,下图展示了有阴影和没有阴影的情况下的不同:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_with_without.png)
你可以看到,有阴影的时候你能更容易地区分出物体之间的位置关系,例如,当使用阴影的时候浮在地板上的立方体的事实更加清晰。
阴影还是比较不好实现的,因为当前实时渲染领域还没找到一种完美的阴影算法。目前有几种近似阴影技术,但它们都有自己的弱点和不足,这点我们必须要考虑到。
视频游戏中较多使用的一种技术是阴影贴图shadow mapping效果不错而且相对容易实现。阴影贴图并不难以理解性能也不会太低而且非常容易扩展成更高级的算法比如 [Omnidirectional Shadow Maps](http://learnopengl.com/#!Advanced-Lighting/Shadows/Point-Shadows)和 [Cascaded Shadow Maps](http://learnopengl.com/#!Advanced-Lighting/Shadows/CSM))。
### 阴影映射
阴影映射背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory.png)
这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment它们应该渲染为带阴影的。如果我们绘制一条从光源出发到达最右边盒子上的一个片元上的线段或射线那么射线将先击中悬浮的盒子随后才会到达最右侧的盒子。结果就是悬浮的盒子被照亮而最右侧的盒子将处于阴影之中。
我们希望得到射线第一次击中的那个物体,然后用这个最近点和射线上其他点进行对比。然后我们将测试一下看看射线上的其他点是否比最近点更远,如果是的话,这个点就在阴影中。对从光源发出的射线上的成千上万个点进行遍历是个极端消耗性能的举措,实时渲染上基本不可取。我们可以采取相似举措,不用投射出光的射线。我们所使用的是非常熟悉的东西:深度缓冲。
你可能记得在[深度测试](http://learnopengl.com/#!Advanced-OpenGL/Depth-testing)教程中在深度缓冲里的一个值是摄像机视角下对应于一个片元的一个0到1之间的深度值。如果我们从光源的透视图来渲染场景并把深度值的结果储存到纹理中会怎样通过这种方式我们就能对光源的透视图所见的最近的深度值进行采样。最终深度值就会显示从光源的透视图下见到的第一个片元了。我们管储存在纹理中的所有这些深度值叫做深度贴图depth map或阴影贴图。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_theory_spaces.png)
左侧的图片展示了一个定向光源所有光线都是平行的在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值我们就能找到最近点用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换它可以将任何三维位置转变到光源的可见坐标空间。
!!! Important
定向光并没有位置,因为它被规定为无穷远。然而,为了实现阴影贴图,我们得从一个光的透视图渲染场景,这样就得在光的方向的某一点上渲染场景。
在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点P处的片元需要决定它是否在阴影中。我们先得使用T把P变换到光源的坐标空间里。既然点P是从光的透视图中看到的它的z坐标就对应于它的深度例子中这个值是0.9。使用点P在光源的坐标空间的坐标我们可以索引深度贴图来获得从光的视角中最近的可见深度结果是点C最近的深度是0.4。因为索引深度贴图的结果是一个小于点P的深度我们可以断定P被挡住了它在阴影中了。
深度映射由两个步骤组成:首先,我们渲染深度贴图,然后我们像往常一样渲染场景,使用生成的深度贴图来计算片元是否在阴影之中。听起来有点复杂,但随着我们一步一步地讲解这个技术,就能理解了。
### 深度贴图depth map
第一步我们需要生成一张深度贴图。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。
首先,我们要为渲染的深度贴图创建一个帧缓冲对象:
```c++
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
```
然后创建一个2D纹理提供给帧缓冲的深度缓冲使用
```c++
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
GLuint depthMap;
glGenTextures(1, &depthMap);
glBindTexture(GL_TEXTURE_2D, depthMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT,
SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, 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_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
```
生成深度贴图不太复杂。因为我们只关心深度值我们要把纹理格式指定为GL_DEPTH_COMPONENT。我们还要把纹理的高宽设置为1024这是深度贴图的解析度。
把我们把生成的深度纹理作为帧缓冲的深度缓冲:
```c++
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
```
我们需要的只是在从光的透视图下渲染场景的时候深度信息所以颜色缓冲没有用。然而帧缓冲对象不是完全不包含颜色缓冲的所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。我们通过将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。
合理配置将深度值渲染到纹理的帧缓冲后,我们就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样:
```c++
// 1. 首选渲染深度贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. 像往常一样渲染场景,但这次使用深度贴图
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_2D, depthMap);
RenderScene();
```
这段代码隐去了一些细节但它表达了阴影映射的基本思路。这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景通常是窗口解析度有着不同的解析度我们需要改变视口viewport的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数最后的深度贴图要么太小要么就不完整。
### 光源空间的变换light spacce transform
前面那段代码中一个不清楚的函数是COnfigureShaderAndMatrices。它是用来在第二个步骤确保为每个物体设置了合适的投影和视图矩阵以及相关的模型矩阵。然而第一个步骤中我们从光的位置的视野下使用了不同的投影和视图矩阵来渲染的场景。
因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形:
```c++
GLfloat near_plane = 1.0f, far_plane = 7.5f;
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
```
这里有个本节教程的demo场景中使用的正交投影矩阵的例子。因为投影矩阵间接决定可视区域的范围以及哪些东西不会被裁切你需要保证投影视锥frustum的大小以包含打算在深度贴图中包含的物体。当物体和片元不在深度贴图中时它们就不会产生阴影。
为了创建一个视图矩阵来变换每个物体把它们变换到从光源视角可见的空间中我们将使用glm::lookAt函数这次从光源的位置看向场景中央。
```c++
glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f), glm::vec3(1.0));
```
二者相结合为我们提供了一个光空间的变换矩阵,它将每个世界空间坐标变换到光源处所见到的那个空间;这正是我们渲染深度贴图所需要的。
```c++
glm::mat4 lightSpaceMatrix = lightProjection * lightView;
```
这个lightSpaceMatrix正是前面我们称为T的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵我们就能像往常那样渲染场景了。然而我们只关心深度值并非所有片元计算都在我们的着色器中进行。为了提升性能我们将使用一个与之不同但更为简单的着色器来渲染出深度贴图。
### 渲染出深度贴图
当我们以光的透视图进行场景渲染的时候我们会用一个比较简单的着色器这个着色器除了把顶点变换到光空间以外不会做得更多了。这个简单的着色器叫做simpleDepthShader就是使用下面的这个着色器
```c++
#version 330 core
layout (location = 0) in vec3 position;
uniform mat4 lightSpaceMatrix;
uniform mat4 model;
void main()
{
gl_Position = lightSpaceMatrix * model * vec4(position, 1.0f);
}
```
这个顶点着色器将一个单独模型的一个顶点使用lightSpaceMatrix变换到光空间中。
由于我们没有颜色缓冲,最后的片元不需要任何处理,所以我们可以简单地使用一个空像素着色器:
```c++
#version 330 core
void main()
{
// gl_FragDepth = gl_FragCoord.z;
}
```
这个空像素着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消那行的注释,来显式设置深度,但是这个(指注释掉那行之后)是更有效率的,因为底层无论如何都会默认去设置深度缓冲。
渲染深度缓冲现在成了:
```c++
simpleDepthShader.Use();
glUniformMatrix4fv(lightSpaceMatrixLocation, 1, GL_FALSE, glm::value_ptr(lightSpaceMatrix));
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
RenderScene(simpleDepthShader);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
```
这里的RenderScene函数的参数是一个着色器程序shader program它调用所有相关的绘制函数并在需要的地方设置相应的模型矩阵。
最后在光的透视图视角下很完美地用每个可见片元的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上和我们在帧缓冲一节做的后处理过程类似就能在屏幕上显示出来我们会获得这样的东西
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_depth_map.png)
将深度贴图渲染到四边形上的像素着色器:
```c++
#version 330 core
out vec4 color;
in vec2 TexCoords;
uniform sampler2D depthMap;
void main()
{
float depthValue = texture(depthMap, TexCoords).r;
color = vec4(vec3(depthValue), 1.0);
}
```
要注意的是当用透视投影矩阵取代正交投影矩阵来显示深度时,有一些轻微的改动,因为使用透视投影时,深度是非线性的。本节教程的最后,我们会讨论这些不同之处。
你可以在[这里](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping_depth_map)获得把场景渲染成深度贴图的源码。
### 渲染阴影
正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在像素着色器中执行,用来检验一个片元是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:
```c++
#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;
vec4 FragPosLightSpace;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;
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;
vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
}
```
这儿的新的地方是FragPosLightSpace这个输出向量。我们用同一个lightSpaceMatrix把世界空间顶点位置转换为光空间。顶点着色器传递一个普通的经变换的世界空间顶点位置vs_out.FragPos和一个光空间的vs_out.FragPosLightSpace给像素着色器。
像素着色器使用Blinn-Phong光照模型渲染场景。我们接着计算出一个shadow值当fragment在阴影中时是1.0在阴影外是0.0。然后diffuse和specular颜色会乘以这个阴影元素。由于阴影不会是全黑的由于散射我们把ambient分量从乘法中剔除。
```c++
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
vec4 FragPosLightSpace;
} fs_in;
uniform sampler2D diffuseTexture;
uniform sampler2D shadowMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
float ShadowCalculation(vec4 fragPosLightSpace)
{
[...]
}
void main()
{
vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = vec3(1.0);
// Ambient
vec3 ambient = 0.15 * 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;
// 计算阴影
float shadow = ShadowCalculation(fs_in.FragPosLightSpace);
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
FragColor = vec4(lighting, 1.0f);
}
```
像素着色器大部分是从高级光照教程中复制过来只不过加上了个阴影计算。我们声明一个shadowCalculation函数用它计算阴影。像素着色器的最后我们我们把diffuse和specular乘以(1-阴影元素),这表示这个片元有多大成分不在阴影中。这个像素着色器还需要两个额外输入,一个是光空间的片元位置和第一个渲染阶段得到的深度贴图。
首先要检查一个片元是否在阴影中把光空间片元位置转换为裁切空间的标准化设备坐标。当我们在顶点着色器输出一个裁切空间顶点位置到gl_Position时OpenGL自动进行一个透视除法将裁切空间坐标的范围-w到w转为-1到1这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里我们必须自己做透视除法
```c++
float ShadowCalculation(vec4 fragPosLightSpace)
{
// 执行透视除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
[...]
}
```
返回了片元在光空间的-1到1的范围。
!!! Important
当使用正交投影矩阵顶点w元素仍保持不变所以这一步实际上毫无意义。可是当使用透视投影的时候就是必须的了所以为了保证在两种投影矩阵下都有效就得留着这行。
因为来自深度贴图的深度在0到1的范围我们也打算使用projCoords从深度贴图中去采样所以我们将NDC坐标变换为0到1的范围
译者注这里的意思是上面的projCoords的xyz分量都是[-1,1]下面会指出这对于远平面之类的点才成立而为了和深度贴图的深度相比较z分量需要变换到[0,1]为了作为从深度贴图中采样的坐标xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。)
```c++
projCoords = projCoords * 0.5 + 0.5;
```
有了这些投影坐标我们就能从深度贴图中采样得到0到1的结果从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。我们将得到光的位置视野下最近的深度
```c++
float closestDepth = texture(shadowMap, projCoords.xy).r;
```
为了得到片元的当前深度我们简单获取投影向量的z坐标它等于来自光的透视视角的片元的深度。
```c++
float currentDepth = projCoords.z;
```
实际的对比就是简单检查currentDepth是否高于closetDepth如果是那么片元就在阴影中。
```c++
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
```
完整的shadowCalculation函数是这样的
```c++
float ShadowCalculation(vec4 fragPosLightSpace)
{
// 执行透视除法
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
// 变换到[0,1]的范围
projCoords = projCoords * 0.5 + 0.5;
// 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标)
float closestDepth = texture(shadowMap, projCoords.xy).r;
// 取得当前片元在光源视角下的深度
float currentDepth = projCoords.z;
// 检查当前片元是否在阴影中
float shadow = currentDepth > closestDepth ? 1.0 : 0.0;
return shadow;
}
```
激活这个着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_shadows.png)
如果你做对了你会看到地板和上有立方体的阴影。你可以从这里找到demo程序的[源码](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping_shadows)。
### 改进阴影贴图
我们试图让阴影映射工作,但是你也看到了,阴影映射还是有点不真实,我们修复它才能获得更好的效果,这是下面的部分所关注的焦点。
#### 阴影失真shadow acne
前面的图片中明显有不对的地方。放大看会发现明显的线条样式:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne.png)
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真,下图解释了成因:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne_diagram.png)
因为阴影贴图受限于解析度,在距离光源比较远的情况下,多个片元可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片元从同一个深度值进行采样。
虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片元就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片元被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。
我们可以用一个叫做**阴影偏移**shadow bias的技巧来解决这个问题我们简单的对表面的深度或深度贴图应用一个偏移量这样片元就不会被错误地认为在表面之下了。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_acne_bias.png)
使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以这样实现这个偏移:
```c++
float bias = 0.005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
```
一个0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:
```c++
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
```
这里我们有一个偏移量的最大值0.05和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_with_bias.png)
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
#### 悬浮
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_peter_panning.png)
这个阴影失真叫做Peter panning因为物体看起来轻轻悬浮在表面之上译注Peter Pan就是童话彼得潘而panning有平移、悬浮之意而且彼得潘是个会飞的男孩…。我们可以使用一个叫技巧解决大部分的Peter panning问题当渲染深度贴图时候使用正面剔除front face culling你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。
因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_culling.png)
为了修复peter游移我们要进行正面剔除先必须开启GL_CULL_FACE
```c++
glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // 不要忘记设回原先的culling face
```
这十分有效地解决了peter panning的问题但只针对实体物体内部不会对外开口。我们的场景中在立方体上工作的很好但在地板上无效因为正面剔除完全移除了地板。地面是一个单独的平面不会被完全剔除。如果有人打算使用这个技巧解决peter panning必须考虑到只有剔除物体的正面才有意义。
另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning。
#### 采样超出
无论你喜不喜欢还有一个视觉差异就是光的视锥不可见的区域一律被认为是处于阴影中不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式我们将会得到不正确的深度结果它不是基于真实的来自光源的深度值。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_outside_frustum.png)
你可以在图中看到光照有一个区域超出该区域就成为了阴影这个区域实际上代表着深度贴图的大小这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。
我们宁可让所有超出深度贴图的坐标的深度范围是1.0这样超出的坐标将永远不在阴影之中。我们可以储存一个边框颜色然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER
```c++
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
```
现在如果我们采样深度贴图0到1坐标范围以外的区域纹理函数总会返回一个1.0的深度值阴影值为0.0。结果看起来会更真实:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_clamp_edge.png)
仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。
当一个点比光的远平面还要远时它的投影坐标的z坐标大于1.0。这种情况下GL_CLAMP_TO_BORDER环绕方式不起作用因为我们把坐标的z元素和深度贴图的值进行了对比它总是为大于1.0的z返回true。
解决这个问题也很简单我们简单的强制把shadow的值设为0.0不管投影向量的z坐标是否大于1.0
```c++
float ShadowCalculation(vec4 fragPosLightSpace)
{
[...]
if(projCoords.z > 1.0)
shadow = 0.0;
return shadow;
}
```
检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终会得到下面我们所追求的效果:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_over_sampling_fixed.png)
这些结果意味着只有在深度贴图范围以内的被投影的fragment坐标才有阴影所以任何超出范围的都将会没有阴影。由于在游戏中通常这只发生在远处就会比我们之前的那个明显的黑色区域效果更真实。
#### PCF
阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对解析度的依赖很快变得很明显。
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_zoom.png)
因为深度贴图有一个固定的解析度,多个片元对应于一个纹理像素。结果就是多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边。
你可以通过增加深度贴图解析度的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。
另一个并不完整的解决方案叫做PCFpercentage-closer filtering这是一种多个不同过滤方式的组合它产生柔和阴影使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起进行平均化我们就得到了柔和阴影。
一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样然后把结果平均起来
```c++
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
```
这个textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小我们用以对纹理坐标进行偏移确保每个新样本来自不同的深度值。这里我们采样得到9个值它们在投影坐标的x和y值的周围为阴影阻挡进行测试并最终通过样本的总数目将结果平均化。
使用更多的样本更改texelSize变量你就可以增加阴影的柔和程度。下面你可以看到应用了PCF的阴影
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_soft_shadows.png)
从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影贴图解析度的不真实感,但通常对于大多数应用来说效果已经很好了。
你可以从[这里](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping)找到这个例子的全部源码和第二个阶段的[顶点](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping&type=vertex)和[片段](http://learnopengl.com/code_viewer.php?code=advanced-lighting/shadow_mapping&type=fragment)着色器。
实际上PCF还有更多的内容以及很多技术要点需要考虑以提升柔和阴影的效果但处于本章内容长度考虑我们将留在以后讨论。
### 正交 vs 投影
在渲染深度贴图的时候,正交和投影矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
![](http://learnopengl.com/img/advanced-lighting/shadow_mapping_projection.png)
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。
另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。发生这个是因为透视投影下,深度变成了非线性的深度值,它的大多数可辨范围接近于近平面。为了可以像使用正交投影一样合适的观察到深度值,你必须先讲过非线性深度值转变为线性的,我们在深度测试教程中已经讨论过。
```c++
#version 330 core
out vec4 color;
in vec2 TexCoords;
uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane;
float LinearizeDepth(float depth)
{
float z = depth * 2.0 - 1.0; // Back to NDC
return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}
void main()
{
float depthValue = texture(depthMap, TexCoords).r;
color = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
// color = vec4(vec3(depthValue), 1.0); // orthographic
}
```
这个深度值与我们见到的用正交投影的很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。
### 附加资源
[Tutorial 16 : Shadow](http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-16-shadow-mapping/)
[mappingopengl-tutorial.org](http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html) 提供的类似的阴影映射教程,里面有一些额外的解释。
[Shadow Mapping Part 1ogldev](http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html)提供的另一个阴影映射教程。
[How Shadow Mapping Works](https://www.youtube.com/watch?v=EsccgeUpdsM)的一个第三方YouTube视频教程里面解释了阴影映射及其实现。
[Common Techniques to Improve Shadow Depth Maps](https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324%28v=vs.85%29.aspx):微软的一篇好文章,其中理出了很多提升阴影贴图质量的技术。

View File

@@ -0,0 +1,481 @@
本文作者JoeyDeVries由Django翻译自[http://learnopengl.com](http://learnopengl.com)
## 点光源阴影(Shadow Mapping)
上个教程我们学到了如何使用阴影映射技术创建动态阴影。效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。所以它也叫定向阴影映射,深度(阴影)贴图生成自定向光的视角。
!!! Important
本节我们的焦点是在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。
这个技术叫做点光阴影过去的名字是万向阴影贴图omnidirectional shadow maps技术。
本节代码基于前面的阴影映射教程,所以如果你对传统阴影映射不熟悉,还是建议先读一读阴影映射教程。
算法和定向阴影映射差不多我们从光的透视图生成一个深度贴图基于当前fragment位置来对深度贴图采样然后用储存的深度值和每个fragment进行对比看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
对于深度贴图我们需要从一个点光源的所有渲染场景普通2D深度贴图不能工作如果我们使用cubemap会怎样因为cubemap可以储存6个面的环境数据它可以将整个场景渲染到cubemap的每个面上把它们当作点光源四周的深度值来采样。
![](http://learnopengl.com/img/advanced-lighting/point_shadows_diagram.png)
生成后的深度cubemap被传递到光照像素着色器它会用一个方向向量来采样cubemap从而得到当前的fragment的深度从光的透视图。大部分复杂的事情已经在阴影映射教程中讨论过了。算法只是在深度cubemap生成上稍微复杂一点。
#### 生成深度cubemap
为创建一个光周围的深度值的cubemap我们必须渲染场景6次每次一个面。显然渲染场景6次需要6个不同的视图矩阵每次把一个不同的cubemap面附加到帧缓冲对象上。这看起来是这样的
```c++
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();
}
```
这会很耗费性能因为一个深度贴图下需要进行很多渲染调用。这个教程中我们将转而使用另外的一个小技巧来做这件事几何着色器允许我们使用一次渲染过程来建立深度cubemap。
首先我们需要创建一个cubemap
```c++
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);
```
然后生成cubemap的每个面将它们作为2D深度值纹理图像
```c++
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);
```
不要忘记设置合适的纹理参数:
```c++
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);
```
正常情况下我们把cubemap纹理的一个面附加到帧缓冲对象上渲染场景6次每次将帧缓冲的深度缓冲目标改成不同cubemap面。由于我们将使用一个几何着色器它允许我们把所有面在一个过程渲染我们可以使用glFramebufferTexture直接把cubemap附加成帧缓冲的深度附件
```c++
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
```
还要记得调用glDrawBuffer和glReadBuffer当生成一个深度cubemap时我们只关心深度值所以我们必须显式告诉OpenGL这个帧缓冲对象不会渲染到一个颜色缓冲里。
万向阴影贴图有两个渲染阶段首先我们生成深度贴图然后我们正常使用深度贴图渲染在场景中创建阴影。帧缓冲对象和cubemap的处理看起是这样的
```c++
// 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();
```
这个过程和默认的阴影映射一样尽管这次我们渲染和使用的是一个cubemap深度纹理而不是2D深度纹理。在我们实际开始从光的视角的所有方向渲染场景之前我们先得计算出合适的变换矩阵。
### 光空间的变换
设置了帧缓冲和cubemap我们需要一些方法来讲场景的所有几何体变换到6个光的方向中相应的光空间。与阴影映射教程类似我们将需要一个光空间的变换矩阵T但是这次是每个面都有一个。
每个光空间的变换矩阵包含了投影和视图矩阵。对于投影矩阵来说,我们将使用一个透视投影矩阵;光源代表一个空间中的点,所以透视投影矩阵更有意义。每个光空间变换矩阵使用同样的投影矩阵:
```c++
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);
```
非常重要的一点是这里glm::perspective的视野参数设置为90度。90度我们才能保证视野足够大到可以合适地填满cubemap的一个面cubemap的所有面都能与其他面在边缘对齐。
因为投影矩阵在每个方向上并不会改变我们可以在6个变换矩阵中重复使用。我们要为每个方向提供一个不同的视图矩阵。用glm::lookAt创建6个观察方向每个都按顺序注视着cubemap的的一个方向右、左、上、下、近、远
```c++
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));
```
这里我们创建了6个视图矩阵把它们乘以投影矩阵来得到6个不同的光空间变换矩阵。glm::lookAt的target参数是它注视的cubemap的面的一个方向。
这些变换矩阵发送到着色器渲染到cubemap里。
### 深度着色器
为了把值渲染到深度cubemap我们将需要3个着色器顶点和像素着色器以及一个它们之间的几何着色器。
几何着色器是负责将所有世界空间的顶点变换到6个不同的光空间的着色器。因此顶点着色器简单地将顶点变换到世界空间然后直接发送到几何着色器
```c++
#version 330 core
layout (location = 0) in vec3 position;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(position, 1.0);
}
```
紧接着几何着色器以3个三角形的顶点作为输入它还有一个光空间变换矩阵的uniform数组。几何着色器接下来会负责将顶点变换到光空间这里它开始变得有趣了。
几何着色器有一个内建变量叫做gl_Layer它指定发散出基本图形送到cubemap的哪个面。当不管它时几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段但当我们更新这个变量就能控制每个基本图形将渲染到cubemap的哪一个面。当然这只有当我们有了一个附加到激活的帧缓冲的cubemap纹理才有效
```c++
#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();
}
}
```
几何着色器相对简单。我们输入一个三角形输出总共6个三角形6*3顶点所以总共18个顶点。在main函数中我们遍历cubemap的6个面我们每个面指定为一个输出面把这个面的interger整数存到gl_Layer。然后我们通过把面的光空间变换矩阵乘以FragPos将每个世界空间顶点变换到相关的光空间生成每个三角形。注意我们还要将最后的FragPos变量发送给像素着色器我们需要计算一个深度值。
上个教程我们使用的是一个空的像素着色器让OpenGL配置深度贴图的深度值。这次我们将计算自己的深度这个深度就是每个fragment位置和光源位置之间的线性距离。计算自己的深度值使得之后的阴影计算更加直观。
```c++
#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 = gl_FragCoord.z;
}
```
像素着色器将来自几何着色器的FragPos、光的位置向量和视锥的远平面值作为输入。这里我们把fragment和光源之间的距离映射到0到1的范围把它写入为fragment的深度值。
使用这些着色器渲染场景cubemap附加的帧缓冲对象激活以后你会得到一个完全填充的深度cubemap以便于进行第二阶段的阴影计算。
### 万向阴影贴图
所有事情都做好了是时候来渲染万向阴影了。这个过程和定向阴影映射教程相似尽管这次我们绑定的深度贴图是一个cubemap而不是2D纹理并且将光的投影的远平面发送给了着色器。
```c++
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函数在一个大立方体房间中渲染一些立方体它们散落在大立方体各处光源在场景中央。
顶点着色器和像素着色器和原来的阴影映射着色器大部分都一样不同之处是在光空间中像素着色器不再需要一个fragment位置现在我们可以使用一个方向向量采样深度值。
因为这个顶点着色器不再需要将他的位置向量变换到光空间所以我们可以去掉FragPosLightSpace变量
```c++
#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光照代码和我们之前阴影相乘的结尾部分一样
```c++
#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);
}
```
有一些细微的不同光照代码一样但我们现在有了一个uniform变量samplerCubeshadowCalculation函数用fragment的位置作为它的参数取代了光空间的fragment位置。我们现在还要引入光的视锥的远平面值后面我们会需要它。像素着色器的最后我们计算出阴影元素当fragment在阴影中时它是1.0不在阴影中时是0.0。我们使用计算出来的阴影元素去影响光照的diffuse和specular元素。
在ShadowCalculation函数中有很多不同之处现在是从cubemap中进行采样不再使用2D纹理了。我们来一步一步的讨论一下的它的内容。
我们需要做的第一件事是获取cubemap的森都。你可能已经从教程的cubemap部分想到我们已经将深度储存为fragment和光位置之间的距离了我们这里采用相似的处理方式
```c++
float ShadowCalculation(vec3 fragPos)
{
vec3 fragToLight = fragPos - lightPos;
float closestDepth = texture(depthMap, fragToLight).r;
}
```
在这里我们得到了fragment的位置与光的位置之间的不同的向量使用这个向量作为一个方向向量去对cubemap进行采样。方向向量不需要是单位向量所以无需对它进行标准化。最后的closestDepth是光源和它最接近的可见fragment之间的标准化的深度值。
closestDepth值现在在0到1的范围内了所以我们先将其转换会0到far_plane的范围这需要把他乘以far_plane
```c++
closestDepth *= far_plane;
```
下一步我们获取当前fragment和光源之间的深度值我们可以简单的使用fragToLight的长度来获取它这取决于我们如何计算cubemap中的深度值
```c++
float currentDepth = length(fragToLight);
```
返回的是和closestDepth范围相同的深度值。
现在我们可以将两个深度值对比一下看看哪一个更接近以此决定当前的fragment是否在阴影当中。我们还要包含一个阴影偏移所以才能避免阴影失真这在前面教程中已经讨论过了。
```c++
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
```
完整的ShadowCalculation现在变成了这样
```c++
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;
}
```
有了这些着色器,我们已经能得到非常好的阴影效果了,这次从一个点光源所有周围方向上都有阴影。有一个位于场景中心的点光源,看起来会像这样:
![](http://learnopengl.com/img/advanced-lighting/point_shadows.png)
你可以从这里找到这个[demo的源码](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows)、[顶点](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows&type=vertex)和[片段](http://www.learnopengl.com/code_viewer.php?code=advanced-lighting/point_shadows&type=fragment)着色器。
#### 把cubemap深度缓冲显示出来
如果你想我一样第一次并没有做对那么就要进行调试排错将深度贴图显示出来以检查其是否正确。因为我们不再用2D深度贴图纹理深度贴图的显示不会那么显而易见。
一个简单的把深度缓冲显示出来的技巧是在ShadowCalculation函数中计算标准化的closestDepth变量把变量显示为
```c++
FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
```
结果是一个灰度场景,每个颜色代表着场景的线性深度值:
![](http://learnopengl.com/img/advanced-lighting/point_shadows_depth_cubemap.png)
你可能也注意到了带阴影部分在墙外。如果看起来和这个差不多你就知道深度cubemap生成的没错。否则你可能做错了什么也许是closestDepth仍然还在0到far_plane的范围。
#### PCF
由于万向阴影贴图基于传统阴影映射的原则它便也继承了由解析度产生的非真实感。如果你放大就会看到锯齿边了。PCF或称Percentage-closer filtering允许我们通过对fragment位置周围过滤多个样本并对结果平均化。
如果我们用和前面教程同样的那个简单的PCF过滤器并加入第三个维度就是这样的
```c+++
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);
```
这段代码和我们传统的阴影映射没有多少不同。这里我们根据样本的数量动态计算了纹理偏移量,我们在三个轴向采样三次,最后对子样本进行平均化。
现在阴影看起来更加柔和平滑了,由此得到更加真实的效果:
![](http://learnopengl.com/img/advanced-lighting/point_shadows_soft.png)
然而samples设置为4.0每个fragment我们会得到总共64个样本这太多了
大多数这些样本都是多余的它们在原始方向向量近处采样不如在采样方向向量的垂直方向进行采样更有意义。可是没有简单的方式能够指出哪一个子方向是多余的这就难了。有个技巧可以使用用一个偏移量方向数组它们差不多都是分开的每一个指向完全不同的方向剔除彼此接近的那些子方向。下面就是一个有着20个偏移方向的数组
```c++
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得到的样本数量进行适配使用它们从cubemap里采样。这么做的好处是与之前的PCF算法相比我们需要的样本数量变少了。
```c++
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);
```
这里我们把一个偏移量添加到指定的diskRadius中它在fragToLight方向向量周围从cubemap里采样。
另一个在这里可以应用的有意思的技巧是我们可以基于观察者里一个fragment的距离来改变diskRadius这样我们就能根据观察者的距离来增加偏移半径了当距离更远的时候阴影更柔和更近了就更锐利。
```c++
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
```
PCF算法的结果如果没有变得更好也是非常不错的这是柔和的阴影效果
![](http://learnopengl.com/img/advanced-lighting/point_shadows_soft_better.png)
当然了我们添加到每个样本的bias偏移高度依赖于上下文总是要根据场景进行微调的。试试这些值看看怎样影响了场景。
这里是最终版本的顶点和像素着色器。
我还要提醒一下使用几何着色器来生成深度贴图不会一定比每个面渲染场景6次更快。使用几何着色器有它自己的性能局限在第一个阶段使用它可能获得更好的性能表现。这取决于环境的类型以及特定的显卡驱动等等所以如果你很关心性能就要确保对两种方法有大致了解然后选择对你场景来说更高效的那个。我个人还是喜欢使用几何着色器来进行阴影映射原因很简单因为它们使用起来更简单。
### 附加资源
[Shadow Mapping for point light sources in OpenGL](http://www.sunandblackcat.com/tipFullView.php?l=eng&topicid=36)sunandblackcat的万向阴影映射教程。
[Multipass Shadow Mapping With Point Lights](http://ogldev.atspace.co.uk/www/tutorial43/tutorial43.html)ogldev的万向阴影映射教程。
[Omni-directional Shadows](http://www.cg.tuwien.ac.at/~husky/RTR/OmnidirShadows-whyCaps.pdf)Peter Houska的关于万向阴影映射的一组很好的ppt。