点光源阴影(Omnidirectional Shadow Maps)
摊牌
- 由点光源产生的阴影,过去的名字是万向阴影贴图(Omnidirectional Shadow Maps)
- 点光源可以照射的方向是空间中的各个方向,因此他产生的阴影也是在空间中的各个方向
原理
- 先回顾一下阴影贴图中提到的绘制阴影的原理:首先在光源的视角下渲染场景,得到一张深度贴图,然后在摄像机的视角下再次渲染场景,并将这时候的顶点变换到光源空间中,先计算顶点在光源空间中的实际深度,再对深度贴图采样,通过实际深度值与最小深度值比较,判断片段是否在阴影中
- 点光源阴影与阴影贴图的原理相似,不同的地方是生成深度贴图的维度
- 因为点光源产生的阴影是在空间中的各个方向,所以我们可以用一张立方体的深度贴图来包含各个方向上的深度值
实现
- 创建立方体贴图的纹理glGenTextures(1, &cubeDepthMap)
- 生成立方体贴图的每个面,并将每个面都设置为2D深度贴图,不要忘记设置环绕方式、过滤方式
for (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);
}
- 需要注意的是:在正常情况下,一次只能将立方体贴图的一个面附加到帧缓冲的附件上,所以需要渲染场景6次,每次把帧缓冲的深度附件改成不同的立方体面,最终完成一个立方体纹理
- 但是如果我们使用几何着色器,则可以把所有的立方体面在一次渲染中完成,这样我们就需要把立方体贴直接附加到帧缓冲的深度附件上
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, cubeDepthMap, 0);
- 还有记得调用glDrawBuffer(GL_NONE)和glReadBuffer(GL_NONE)来显示的告诉OpenGL这个帧缓冲对象不会渲染到颜色缓冲
- 光源空间的变换
- 对于投影矩阵来说,我们使用透视投影;因为点光源代表一个空间中的点,使用透视投影更有实际意义
- 对于不同立方体面使用同一个投影矩阵
- 需要注意的是:透视投影的视角需要设置成90度,这样才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐
- 对于观察矩阵,需要为立方体贴图的6个面设置不同的观察矩阵
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));
//观察矩阵的up参数,为什么要朝向那个方向?
- 顶点着色器1
- 由于我们打算使用几何着色器来实现立方体深度贴图,所以先设想一下几何着色器要做的事情,这样就顺带确定了顶点着色器要做的事情。几何着色器要做的就是将世界空间中的顶点变换到6个不同的光空间中,因此顶点着色器只需要将顶点变换到世界空间即可
gl_Position = modelMat * vec4(position, 1.0);
- 几何着色器1
- 几何着色器将一次接收到三角面的三个顶点,和一个光空间的变换矩阵数据
- 几何着色器的内置变量gl_Layer,指定发射出的基本图形将发送到立方体的哪个面上
- 这样,有了顶点、变换矩阵和gl_Layer就可以将同一个三角面变换后发送到不同的面上,以至最后构成立方体贴图
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices=18) out;
uniform mat4 shadowMatrices[6];
out vec4 FragPos;
for (int face = 0; face < 6; ++face)
{
gl_Layer = face;
for (int i = 0; i < 3; ++i)
{
FragPos = gl_in[i].gl_Position;
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
- 几何着色器的最后还要将世界空间的顶点输出到片段着色器,因为要在片段着色器中计算深度值
- 片段着色器1
- 片段着色器的目的就是计算深度值,这个深度值是每个片段位置和光源之间的线形距离,然后再变换到[0.0, 1.0]的范围内
- 经过上面7步,可以得到一个立方体深度贴图,下面开始渲染阴影
- 顶点着色器2
- 由于片段着色器2中将使用顶点与光源之间的方向对立方体贴图采样,所以在顶点着色器2中将世界空间中的顶点传给片段着色器2即可
- 片段着色器2
- 片段着色器2主要有三部分工作组成:一是计算光照,这里用Blinn-Phong光照模型;二是判断片段是否在阴影中;三是计算片段最终的颜色
- 判断片段是否在阴影中,需要两个值的比较,一个是当前片段与光源的实际距离,另一个是从立方体贴图中采样到的最小距离,如果实际距离大于最小距离,就说明片段在阴影中
- 对立方体贴图采样用的采样器类型是samplerCube
vec3 fragToLight = fragPos - lightPos;
currentDepth = length(fragToLight); //实际距离
float closestDepth = texture(depthMap, fragToLight).r;
closestDepth *= far_plane; //最近距离
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
显示立方体深度贴图
- 直接对立方体贴图采样,并把采样结果传给gl_FrogColor显示即可
副作用
锯齿
- 由于立方体深度贴图基于传统阴影映射的原理,所以他也继承了由解析度产生的非真实感,因此也会有锯齿的阴影边缘
- 解决办法:减少采样次数的PFC算法
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)
);
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);
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/a1a2201bfda79ebf7919007d7197282f.gif)
- 需要注意的是:使用几何着色器来生成深度贴图不会一定比渲染场景6次更快。使用几何着色器有他自己的性能局限,在第一个阶段使用几何着色器可能获得更好的性能表现,但这也取决于环境的类型,以及特定的显卡驱动等因素