LearnOpenGL学习笔记—高级光照 03(2):点阴影
【项目地址:点击这里这里这里】
本节对应官网学习内容:点阴影
1 万向阴影贴图
上个笔记我们学到了如何使用阴影映射技术创建动态阴影。
虽然效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。
深度(阴影)贴图生成自定向光的视角,所以它也叫定向阴影映射。
- 本节我们的焦点是在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。
这个技术叫做点光阴影,过去的名字是万向阴影贴图(omnidirectional shadow maps)技术。
本节代码基于前面的阴影映射教程。
算法和定向阴影映射差不多:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。
定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图显然是不能做到的。
但是如果我们使用立方体贴图会怎样?
因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。
生成后的深度立方体贴图被传递到光照的片段着色器,它会用一个方向向量来采样立方体贴图,从而得到当前的fragment的深度(从光的透视图)。
期间大部分复杂的事情已经在阴影映射教程中有所阐述,算法只是在深度立方体贴图生成上稍微复杂一点。
1.1 生成深度立方体贴图
为创建一个能体现一个点光周围的深度值的立方体贴图,我们必须渲染场景6次:每次一个面。
显然渲染场景6次需要6个不同的视图矩阵,每次把一个不同的立方体贴图面附加到帧缓冲对象上,然后渲染场景。
举个例子来说比如这样:
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();
}
这会很耗费性能,因为每做一个深度贴图需要进行很多渲染调用。
于是我们将转而使用另外的一个小技巧来做这件事,几何着色器允许我们使用一次渲染过程来建立深度立方体贴图。
下面是我们的步骤(略去在这之前的相机宣告,输入检测,开窗,建立shader,设置材质,宣告光源等步骤)
首先,建立深度贴图的FBO
// 设置深度贴图的FBO
GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);
而后建立立方体贴图
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
为生成这个立方体贴图的每个面,它们会作为深度值的2D纹理图像
const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
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);
我们本来是要渲染场景六次,每次渲染到不同的面上,从而得到这个立方体贴图,但是我们会在之后用几何着色器,它会让我们只在一个过程里渲染。
所以我们这直接把这个立方体贴图作为帧缓冲的深度附件
// 把立方体贴图附加成帧缓冲的深度附件
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "Framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
在生成万象阴影贴图时,我们会有两个阶段:
一个阶段是生成深度贴图,我们把光源看到的视角渲染到深度贴图里
第二个阶段是使用深度贴图渲染,从场景中创建阴影。
抽象来看代码会是这个结构:
// 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_CUBE_MAP, depthCubemap);
RenderScene();
这个过程和之前的阴影映射一样,虽然这次是立方体贴图的深度纹理。
那么接下来我们还是需要计算出合适的变换矩阵,来渲染六个方向的深度贴图
1.2 光空间的变换
与阴影映射教程类似,我们将需要光空间的变换矩阵T,但是这次是每个面都有一个。
每个光空间的变换矩阵包含了投影和视图矩阵。
对于投影矩阵来说,我们将使用透视投影矩阵;因为光源代表一个空间中的点,所以透视投影矩阵更有意义。
每个光空间变换矩阵使用同样的投影矩阵:
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);
这里glm::perspective的视野参数,设置为90度。
90度我们才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐。
因为投影矩阵在每个方向上并不会改变,我们可以在6个变换矩阵中重复使用。
之后我们要为每个方向提供一个不同的视图矩阵。
用glm::lookAt创建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)));
这里我们创建了6个视图矩阵,把它们乘以投影矩阵,来得到6个不同的光空间变换矩阵。
glm::LookAt函数需要一个原始位置、一个目标位置和表示世界空间中的上向量的向量。
这些变换矩阵发送到着色器渲染到立方体贴图里。
1.3 深度着色器
为了把值渲染到深度立方体贴图,我们将需要3个着色器:顶点和像素着色器,以及一个它们之间的几何着色器。
顶点着色器简单地将顶点变换到世界空间,然后直接发送到几何着色器:
#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,它指定发散出基本图形送到立方体贴图的哪个面。
当不管它时,几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段,但当我们更新这个变量就能控制每个基本图形将渲染到立方体贴图的哪一个面。
我们输入一个三角形,输出总共6个三角形(6*3顶点,所以总共18个顶点)。
在main函数中,我们遍历立方体贴图的6个面,我们每个面指定为一个输出面,把这个面的interger(整数)存到gl_Layer。
然后,我们通过把面的光空间变换矩阵乘以FragPos,将每个世界空间顶点变换到相关的光空间,生成每个三角形。
注意,我们还要将最后的FragPos变量发送给像素着色器,我们需要计算一个深度值。
#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();
}
}
这次我们将计算自己的深度,这个深度就是每个fragment位置和光源位置之间的线性距离。计算自己的深度值使得之后的阴影计算更加直观。
#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、光的位置和视锥的远平面值作为输入。
这里我们把fragment和光源之间的距离,映射到0到1的范围,把它写入为fragment的深度值。
使用这些着色器渲染场景,然后在立方体贴图附加的帧缓冲对象激活以后,我们会得到一个完全填充的深度立方体贴图,以便于进行第二阶段的阴影计算。
1.4 生成万向阴影贴图
所有事情都做好了,是时候来渲染万向阴影(Omnidirectional Shadow)了。
尽管这次我们绑定的深度贴图是一个立方体贴图,并且将光的投影的远平面发送给了着色器,但是这个过程和定向阴影映射教程是相似的。
抽象地用代码表示
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.Use();
// 。。。传递uniform
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... 绑定其他uniform
RenderScene();
这里的RenderScene函数含义是在一个大立方体房间中渲染一些立方体,它们散落在大立方体各处,光源在场景中央。
顶点着色器和像素着色器和原来的阴影映射着色器大部分都一样:不同之处是在光空间中像素着色器不再需要一个fragment位置,现在我们可以使用一个方向向量采样深度值。
为了大立方体内被照亮,我们会翻转法线。
顶点着色器:
#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;
uniform int reverse_normals;
void main()
{
gl_Position = projection * view * model * vec4(position, 1.0f);
vs_out.FragPos = vec3(model * vec4(position, 1.0));
if(reverse_normals==0){
vs_out.Normal = transpose(inverse(mat3(model))) * normal;
}else{
vs_out.Normal = -(transpose(inverse(mat3(model))) * normal);
}
vs_out.TexCoords = texCoords;
}
片段着色器:
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec3 Normal