目录
本章节源码 点击此处
一 为什么采用透视投影
透视投影:
- 由于点光源是一个点向四周发散的光线,所以这将导致点光源会以不同的角度到达场景中的不同表面,造成近大远小的效果,所以要采用透视投影矩阵来处理点光源的阴影,透视投影能够正确反映这种随着距离增加而大小和亮度逐渐变化的现象。
- 使用透视投影来渲染点光源的阴影,可以准确地模拟出光源的真实效果,即光源距离物体越远,投射在地面或物体上的阴影边缘越模糊,产生自然的衰减和透视缩放效果。
二 阴影计算思路
- 对于点光源的阴影计算,我们计算方式还是和平行光产生的阴影计算方式是相同的,
- 从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。
- 但是对于点光源要值考虑的一点是,它是从任何方向都会发散的,那么就需要对整个场景中点光源处的六个方向都进行深度贴图的采样。
三 深度贴图生成
对于点光源的深度贴图,我们需要在上下左右前后六个方向都生成深度贴图
3.1 渲染场景生成
- 我们可以在CPU端,渲染深度贴图时,进行6个不同方向的渲染,具体思路就是提供分别由视点方向看下前后左右上下6个方向的观察矩阵,并进行6次深度贴图的渲染,这样就会在深度缓冲中生成了不同方向的深度贴图。
- 但是这种会导致在CPU端进行了多次的渲染调用,这会很消耗CPU的性能。所以我们本章节采用的是3.2(几何着色器的方式来生成深度贴图)这个方式只需要了解即可。
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();
}
3.2 几何着色器生成
我们可以利用几何着色器的功能,在一次渲染过程中就完成多个方向上的深度立方体贴图。我们生成一个深度缓冲,后续利用这个深度缓冲来进行深度值的对比。
3.2.1 创建帧缓冲对象
- 首先我们需要创建一个深度缓冲对象。
unsigned int depthCubeMapFBO;
glGenFramebuffers(1,&depthCubeMapFBO);
3.2.2 准备立方体纹理贴图
- 首先我们需要创建一个纹理对象,并将其绑定到GL_TEXTURE_CUBE_MAP(立方体贴图纹理)
- 使用for循环为立方体贴图的六个面分别设置纹理的格式,这和正常的纹理贴图步骤是一样的,需要先设置纹理贴图的格式,这里要注意的是,对于OpenGL中深度纹理贴图每个面从0-5分别代表着:
0
: 正对X轴的右面(Right)1
: 背对X轴的左面(Left)2
: 正对Y轴的上面(Top)3
: 背对Y轴的下面(Bottom)4
: 正对Z轴的前面(Front)5
: 背对Z轴的后面(Back)- 接着我们要设置纹理的过滤方式。
unsigned int depthCubeMap;
const unsigned int SHADOW_WIDTH = 1024 * 2, SHADOW_HEIGHT = 1024 * 2;
glGenTextures(1,&depthCubeMap);
// 绑定纹理 并设置每个方向上纹理格式
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);
// 设置纹理的过滤方式。
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);
- 正常情况下,我们把立方体贴图纹理的一个面附加到帧缓冲对象上,渲染场景6次,每次将帧缓冲的深度缓冲目标改成不同立方体贴图面。由于我们将使用一个几何着色器,它允许我们把所有面在一个过程渲染,我们可以使用glFramebufferTexture直接把立方体贴图附加成帧缓冲的深度附件。
- 对于一个完整的可以运行的帧缓冲对象,至少要需要一个颜色附件,但是我们这里并不需要颜色的输出和输入我们只需要深度,所以需要禁用当前帧缓冲对象的颜色绘制和读取功能。
- 读取操作不涉及任何颜色数据。这在进行深度测试、模板测试、只关注非颜色附件的渲染任务等场景中是合理的
- 最后一步我们要把帧缓冲对象还原到默认的帧缓冲对象上。对于Qt需要调用default方法,而原始OpenGL在C++设置为0即可。
// 将创建的立方体贴图提供给帧缓冲作为深度附件
glBindFramebuffer(GL_FRAMEBUFFER,depthCubeMapFBO);
// 将纹理附件depthCubeMap作为深度缓冲(GL_DEPTH_ATTACHMENT)绑定到帧缓冲对象上
glFramebufferTexture(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,depthCubeMap,0);
// 别禁用了当前帧缓冲对象的颜色绘制和读取功能,使得后续的渲染和像素
// 读取操作不涉及任何颜色数据。这在进行深度测试、模板测试、只关注非颜色附件的渲染任务等场景中是合理的
// 显示告诉OpenGL不适用颜色进行渲染
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
qDebug() << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER,defaultFramebufferObject());
3.2.3 生成立方体纹理贴图
- 首先我们应该准备一个透视投影矩阵,这个透视投影矩阵是用来生成深度贴图的,我们会利用这个透视投影矩阵中各个顶点的深度值最终和我们真实要渲染的顶点的深度值来对比从而形成阴影。
- 其次我们需要准备6个不同方向的观察矩阵,我们的透视投影矩阵是不会改变的,因为他其实就是代表了一个裁剪空间的范围,而我们需要在6个不同方向的都进行裁剪。
- perspective的视野参数:设置为90度。90度我们才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐。
QMatrix4x4 shadowProj;
QMatrix4x4 shadowView;
float near_plane = 1.0f, far_plane = 25.0f;
// 定义一个透视投影 这个透视投影矩阵是深度缓冲中的裁剪空间 并且这个并不会在每个方向上改变,改变的只是观察矩阵
shadowProj.perspective(90.0f, (float)SHADOW_WIDTH / (float)SHADOW_HEIGHT, near_plane, far_plane);
// 准备6个不同方向的观察矩阵
std::vector<QMatrix4x4> shadowTransforms;
shadowView.lookAt(lightPos, lightPos + QVector3D( 1.0, 0.0, 0.0), QVector3D(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * shadowView); shadowView.setToIdentity();
shadowView.lookAt(lightPos, lightPos + QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * shadowView); shadowView.setToIdentity();
shadowView.lookAt(lightPos, lightPos + QVector3D( 0.0, 1.0, 0.0), QVector3D(0.0, 0.0, 1.0));
shadowTransforms.push_back(shadowProj * shadowView); shadowView.setToIdentity();
shadowView.lookAt(lightPos, lightPos + QVector3D( 0.0,-1.0, 0.0), QVector3D(0.0, 0.0,-1.0));
shadowTransforms.push_back(shadowProj * shadowView); shadowView.setToIdentity();
shadowView.lookAt(lightPos, lightPos + QVector3D( 0.0, 0.0, 1.0), QVector3D(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * shadowView); shadowView.setToIdentity();
shadowView.lookAt(lightPos, lightPos + QVector3D( 0.0, 0.0,-1.0), QVector3D(0.0,-1.0, 0.0));
shadowTransforms.push_back(shadowProj * shadowView);
// 设置绘制的视窗大小,因为这个是绘制在缓冲中的,并不是真正的绘制在屏幕上面的,所以我们最好保持它和深度纹理贴图采样的分辨率一样是最好的
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
// 将当前帧缓冲区绑定到depthCubeMapFBO上
glBindFramebuffer(GL_FRAMEBUFFER, depthCubeMapFBO);
// 清除当前缓冲区的信息
glClear(GL_DEPTH_BUFFER_BIT);
// 绑定深度缓冲的shader
depthProgramObject.bind();
// 将6个观察矩阵传递给GPU端
for (unsigned int i = 0; i < 6; ++i){
std::string str="shadowMatrices[" + std::to_string(i) + "]";
depthProgramObject.setUniformValue(str.c_str(), shadowTransforms[i]);
}
// 将远平面传递给GPU端
depthProgramObject.setUniformValue("far_plane", far_plane);
// 将点光源的位置传递给GPU端
depthProgramObject.setUniformValue("lightPos", lightPos);
// 渲染场景
renderScene(&depthProgramObject);
// 将帧缓冲区绑定到默认的缓冲区对象上,这样才能让后续的绘制绘制到屏幕上面
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());
depthProgramObject.release();
3.2.4 真正的渲染部分
- 这里有很重要的一步,就是在渲染前一个要将GPU端的数据传递过去,尤其是depthCubeMap的设置
- 首先需要绑定纹理单元,然后在将立方体深度贴图传递给GPU端。才能进行渲染操作。
// 绑定shader
shaderProgramObject.bind();
// 生成透视投影,这个透视投影矩阵是真正的视角的裁剪空间
projection.perspective(m_camera.Zoom,(float)width()/(float)height(),_near,_far);
// 获得摄像机的观察视角矩阵
view = m_camera.GetViewMatrix();
// 设置视窗大小,这里是真实的绘制到屏幕上面,所以要保持和openGL的绘制窗口相同。
glViewport(0, 0, width(), height());
// 传递数据到GPU端
shaderProgramObject.setUniformValue("far_plane", far_plane);
shaderProgramObject.setUniformValue("lightPos", lightPos);
shaderProgramObject.setUniformValue("shadows", true);
shaderProgramObject.setUniformValue("projection", projection);
shaderProgramObject.setUniformValue("view", view);
shaderProgramObject.setUniformValue("viewPos",m_camera.Position);
shaderProgramObject.setUniformValue("depthCubeMap",1);
// 绑定纹理单元
glActiveTexture(GL_TEXTURE1);
// 将纹理绑定到纹理单元上
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubeMap);
renderScene(&shaderProgramObject);
shaderProgramObject.release();
3.2.5 深度缓冲shader
- 顶点着色器:因为我们在几何着色器中需要将世界坐标系转换为不同方向的光空间(从光源看往外看的坐标空间)的坐标,所以我们只需要把世界坐标传递过去即可。这里切记我们传递的是世界坐标。
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
void main()
{
gl_Position = model * vec4(aPos, 1.0);
}
- 几何着色器:首先几何着色器中我们以三个顶点作为几何着色器的输入,这意味着geometry shader接收到的是由顶点着色器处理后的三个顶点组成的三角形。
- 设置输出图元类型为三角形带(triangle strip),并声明最大输出顶点数为18。这意味着着色器最多能输出18个顶点来构成新的图元。这样我们就能再几何着色器中一次性渲染6个面
- gl_Layer:设置渲染到的纹理层(对于立方体贴图,每一面是一个单独的层)。这样可以确保当前处理的顶点被定向到正确的立方体贴图面。这只有当我们有了一个附加到激活的帧缓冲的立方体贴图纹理才有效:
- 我们这里把FragPos由世界坐标转换到光空间坐标并且传递给下一个着色器程序处理。
- 因为只有这样才能够在每个面的立方体贴图纹理上生成对应的值。否则如果不变化视角,那么一直看向一个放下,那么6个面的深度纹理采样的数据就会出现问题,只会有一个面是正确的。
#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;
for(int i = 0; i < 3; ++i)
{
FragPos = gl_in[i].gl_Position;
// 每个世界空间顶点变换到相关的光空间
gl_Position = shadowMatrices[face] * FragPos;
EmitVertex();
}
EndPrimitive();
}
}
- 片段着色器: 在片段着色器中我们需要计算光源到顶点的距离,但是要移除w分量的影响
- 然后我们要将它变换到0-1标准坐标中,因为最终写入的深度缓冲是0-1的范围。
- 然后将这个传递给深度值就可以了。
#version 330 core
in vec4 FragPos;
uniform vec3 lightPos;
uniform float far_plane;
void main()
{
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;
}
3.2.6 实际渲染的shader
- 因为我们在立方体贴图的深度缓冲中,我们已经将深度储存为fragment和光位置之间的距离了,所以不再转换到光空间去计算了
- 这里要注意的是,因为我们现在模拟的是房间内的电光源照射的阴影效果,那么最外面这个盒子也就是房间的法向量就不应该再指向外面,而是应该取反指向房间内。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
out vec2 TexCoords;
out VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} vs_out;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform bool reverse_normals;
void main()
{
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
if(reverse_normals) // a slight hack to make sure the outer large cube displays lighting from the 'inside' instead of the default 'outside'.
vs_out.Normal = transpose(inverse(mat3(model))) * (-1.0 * aNormal);
else
vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
vs_out.TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
- 接下来最后一步我们在片段着色器对深度缓冲中的深度值,而当前的深度值进行对比,从而渲染出我们想要的效果。
- 要值的注意的是下面代码中bias这个偏移量是很重要的,这个和之前的阴影逻辑是一样的。都是为了避免产生黑白交错的条纹。
#version 330 core
out vec4 FragColor;
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoords;
} fs_in;
struct Material {
sampler2D texture_diffuse1;
};
uniform Material material;
uniform samplerCube depthMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform float far_plane;
uniform bool shadows;
float ShadowCalculation(vec3 fragPos)
{
// 获取片段位置和灯光位置之间的向量
vec3 fragToLight = fragPos - lightPos;
// 从深度缓冲中获取当前顶点的深度值
float closestDepth = texture(depthMap, fragToLight).r;
// 它当前处于 [0,1] 之间的线性范围内,让我们将其重新转换回原始深度值
closestDepth *= far_plane;
// 当前的顶点的深度值
float currentDepth = length(fragToLight);
// 这个Bias和之前的阴影一样 是为了不让墙壁出现黑白交错的条纹
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
// 输出深度
//FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
return shadow;
}
void main()
{
vec3 color = texture(material.texture_diffuse1, fs_in.TexCoords).rgb;
vec3 normal = normalize(fs_in.Normal);
vec3 lightColor = vec3(0.4);
// ambient
vec3 ambient = 0.3 * lightColor;
// 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 = shadows ? ShadowCalculation(fs_in.FragPos) : 0.0;
vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;
FragColor = vec4(lighting, 1.0);
}
四 效果
- 我们会注意到在墙外会有阴影,这说明立方体贴图生成的是正确的,试想一下它只是个纹理贴图就会明白了。
4.1 深度缓冲
- 我们可以将上面main函数中最终计算的FragColor注释掉,将计算阴影函数中的FragColor的注释代码打开,这样我们就会只输出当前深度缓冲的值