LearnOpenGL学习笔记——阴影

learnOpenGL 5.2 阴影贴图 阴影贴图的原理较为简单,就是以光的方向为视角生成一副深度贴图,这幅图中能看到的部分就是光能照到的部分,所以在计算阴影时只要将片段深度与这幅深度贴图进行对比,深度比它大的片段即在阴影中。 实际实现的步骤如下: 创建画面元素(箱子,地板等)的VBO,VAO,光照矩阵,传入深度贴图shader 注意,因为本节考虑定向光,不存在透视,所以使用正交投影矩阵 GLfloat near_plane = 1.0f, far_plane = 7.5f; glm::mat4 lightProjection = glm::o 阅读详情

阴影

阴影是光线遮挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体表面的时候,那么这个物体就在阴影中了。
在这里插入图片描述
阴影可以让场景更加真实

阴影映射(Shadow Mapping)

阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
在这里插入图片描述
在深度缓冲里的一个值是摄像机视角下,对应于一个片段的一个0到1之间的深度值。如果我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中会怎样?通过这种方式,我们就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片段了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图。简单来说,就是站在光源的位置去看向场景,那么我们能看到的z就是不被遮挡的物体,看不到的位置都是被遮挡的。
在这里插入图片描述
左侧的图片展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片段是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。

在右边的图中我们显示出同样的平行光和观察者。我们渲染一个点P¯处的片段,需要决定它是否在阴影中。我们先得使用T把P¯变换到光源的坐标空间里。既然点P¯是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点P¯在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C¯,最近的深度是0.4。因为索引深度贴图的结果是一个小于点P¯的深度,我们可以断定P¯被挡住了,它在阴影中了。
深度贴图
第一步我们需要生成一张深度贴图(Depth Map)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。首先,我们要为渲染的深度贴图创建一个帧缓冲对象:

GLuint depthMapFBO;
glGenFramebuffers(1, &depthMapFBO);

然后,创建一个2D纹理,提供给帧缓冲的深度缓冲使用:

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:这是深度贴图的分辨率。
把我们把生成的深度纹理作为帧缓冲的深度缓冲:

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来做这件事。合理配置将深度值渲染到纹理的帧缓冲后,我们就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样:

// 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)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。

光源空间的变换
因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形:

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函数;这次从光源的位置看向场景中央。

glm::mat4 lightView = glm::lookAt(glm::vec(-2.0f, 4.0f, -1.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

二者相结合为我们提供了一个光空间的变换矩阵,它将每个世界空间坐标变换到光源处所见到的那个空间;这正是我们渲染深度贴图所需要的。

glm::mat4 lightSpaceMatrix = lightProjection * lightView;

渲染至深度贴图
当我们以光的透视图进行场景渲染的时候,我们会用一个比较简单的着色器,这个着色器除了把顶点变换到光空间以外,不会做得更多了。这个简单的着色器叫做simpleDepthShader,就是使用下面的这个着色器:

#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变换到光空间中。
由于我们没有颜色缓冲,最后的片段不需要任何处理,所以我们可以简单地使用一个空片段着色器:

#version 330 core
void main()
{             
    // gl_FragDepth = gl_FragCoord.z;
}

这个空片段着色器什么也不干,运行完后,深度缓冲会被更新。我们可以取消那行的注释,来显式设置深度,但是这个(指注释掉那行之后)是更有效率的,因为底层无论如何都会默认去设置深度缓冲。
渲染深度缓冲现在成了:

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四边形上(和我们在帧缓冲一节做的后处理过程类似),就能在屏幕上显示出来,我们会获得这样的东西:

在这里插入图片描述

将深度贴图渲染到四边形上的片段着色器:

#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);
}

要注意的是当用透视投影矩阵取代正交投影矩阵来显示深度时,有一些轻微的改动,因为使用透视投影时,深度是非线性的。本节教程的最后,我们会讨论这些不同之处。

总结一(如何获取深度图或者说如何计算遮挡关系)

1.如何渲染阴影
渲染阴影的第一步是计算遮挡关系,一个平行光源,它的方向是一定的,我们从平行光源照向场景。计算出一张遮挡图,也就是把光源当作摄像机的深度图。这样我们就计算出了遮挡关系。
2.如何渲染深度图
利用离屏渲染。其实就是帧缓冲技术,渲染深度信息到纹理
3.使用正交投影计算
透视投影的深度不是线性的

渲染阴影

正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在片段着色器中执行,用来检验一个片段是否在阴影之中,不过我们在顶点着色器中进行光空间的变换:

#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;
    //顶点着色器输出lightspace位置 这里是正交投影
    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分量从乘法中剔除。

#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传到片段着色器里,我们必须自己做透视除法:

float ShadowCalculation(vec4 fragPosLightSpace)
{
    // 执行透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    [...]
}

当使用正交投影矩阵,顶点w元素仍保持不变,所以这一步实际上毫无意义。可是,当使用透视投影的时候就是必须的了,所以为了保证在两种投影矩阵下都有效就得留着这行。

因为来自深度贴图的深度在0到1的范围,我们也打算使用projCoords从深度贴图中去采样,所以我们将NDC坐标变换为0到1的范围。 (译者注:这里的意思是,上面的projCoords的xyz分量都是[-1,1](下面会指出这对于远平面之类的点才成立),而为了和深度贴图的深度相比较,z分量需要变换到[0,1];为了作为从深度贴图中采样的坐标,xy分量也需要变换到[0,1]。所以整个projCoords向量都需要变换到[0,1]范围。)

projCoords = projCoords * 0.5 + 0.5;

有了这些投影坐标,我们就能从深度贴图中采样得到0到1的结果,从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。我们将得到光的位置视野下最近的深度:

float closestDepth = texture(shadowMap, projCoords.xy).r;

为了得到片段的当前深度,我们简单获取投影向量的z坐标,它等于来自光的透视视角的片段的深度。

float currentDepth = projCoords.z;

实际的对比就是简单检查currentDepth是否高于closetDepth,如果是,那么片段就在阴影中。

float shadow = currentDepth > closestDepth  ? 1.0 : 0.0;

完整的shadowCalculation函数是这样的:

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;}

在这里插入图片描述

总结二

1.如何渲染阴影
首先渲染一张深度纹理(以光源位置为相机位置,光源方向为相机朝向),使用正交投影。
我们将渲染好的深度图传入shader,就可以计算阴影了
2.如何计算阴影
对于每一个片元,我们需要计算是否被遮挡。我们有深度图,有该片元在光源投影中的z。比较深度z和深度的信息,就可以计算出遮挡关系。

改进阴影贴图

为了好辨认,使用了与上述不同的纹理
在这里插入图片描述
阴影失真
我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne),下图解释了成因:
在这里插入图片描述
因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片段从同一个深度值进行采样。

虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片段就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片段被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。

我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了。
在这里插入图片描述
使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以这样实现这个偏移:

float bias = 0.005;
float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;

使用0.005的偏移后
在这里插入图片描述

一个0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

这里我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好:
在这里插入图片描述
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。

悬浮(悬浮问题使用剔除反而场景不对,在整个阴影优化的流程中,我自己这里其实没有用到剔除,如果后续有新的理解,再补充)
使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):(ps:上的learnopengl的原图,我自己的场景不太看的出来)
在这里插入图片描述
这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上(译注Peter Pan就是童话彼得潘,而panning有平移、悬浮之意,而且彼得潘是个会飞的男孩…)。我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。
因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。

在这里插入图片描述

	glCullFace(GL_FRONT);
	RenderSceneToDepthMap();
	glCullFace(GL_BACK); // 不要忘记设回原先的culling face

这十分有效地解决了peter panning的问题,但只对内部不会对外开口的实体物体有效。我们的场景中,在立方体上工作的很好,但在地板上无效,因为正面剔除完全移除了地板。地面是一个单独的平面,不会被完全剔除。如果有人打算使用这个技巧解决peter panning必须考虑到只有剔除物体的正面才有意义。
另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning。

采样过多
无论你喜不喜欢还有一个视觉差异,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
在这里插入图片描述
你可以在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。

我们宁可让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。我们可以储存一个边框颜色,然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER:

	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。结果看起来会更真实:
在这里插入图片描述
仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。

当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER环绕方式不起作用,因为我们把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。解决这个问题也很简单,只要投影向量的z坐标大于1.0,我们就把shadow的值强制设为0.0:

	float ShadowCalculation(vec4 fragPosLightSpace)
	{
    [...]
    if(projCoords.z > 1.0)
        shadow = 0.0;

    return shadow;
	}

检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终会得到下面我们所追求的效果:
在这里插入图片描述
PCF
阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对分辨率的依赖很快变得很明显。

在这里插入图片描述
因为深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。你可以通过增加深度贴图的分辨率的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。
另一个(并不完整的)解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:

	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;

在这里插入图片描述
阴影看起来要软一些,放大后
在这里插入图片描述

正交 vs 投影

在渲染深度贴图的时候,正交(Orthographic)和投影(Projection)矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线/光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
在这里插入图片描述
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。发生这个是因为透视投影下,深度变成了非线性的深度值,它的大多数可辨范围都位于近平面附近。为了可以像使用正交投影一样合适地观察深度值,你必须先将非线性深度值转变为线性的,如我们在深度测试教程中已经讨论过的那样。

#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
}

这个深度值与我们见到的用正交投影的很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。

总结三 如何渲染好的阴影

1.引入阴影后的失真是怎么产生的,如何应对失真
2.悬浮问题如何解决 这个不是很理解,待定
3.采样过多问题
4.生成深度图时候,用正交投影还是透视投影

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/

LearnOpenGL CN学习笔记(一) 懒得弄 学习笔记而已 阅读详情

相关推荐

OpenGL阴影贴图实现

在计算机图形学中,阴影是一个关键的效果,它可以使场景更加真实,增强场景的深度感和现实感。其中,阴影贴图是实现阴影效果的一种方法,它利用深度缓冲和光线投射技术来计算场景中物体的阴影,是实现实时阴影效果的常用技术之一。渲染深度值时,需要使用着色器来计算从光源处看到的场景深度信息,并将其存储到帧缓冲的深度纹理中。阴影贴图的基本思想是将光源处看到的场景深度信息渲染到纹理中,然后在场景中使用该纹理来模拟光线在场景中的传播和物体相对于光源的位置和形状。// 创建帧缓冲和深度纹理。// 渲染深度着色器。

qq_39605374的博客 403

openGL阴影实现(硬阴影)

openGL阴影贴图,硬阴影,会产生伪影

aoxuestudy的专栏 5639

《OpenGL阴影指南》——在OpenGL中实现实时阴影的方法

在游戏开发中,实时生成阴影是非常重要的一点。OpenGL提供了多种方法可以实现实时阴影效果,其中比较流行的是阴影贴图(Shadow Mapping)算法。下面将详细介绍在OpenGL中如何使用阴影贴图算法实现实时阴影。接下来,我们需要渲染深度贴图。首先,我们需要对着色器代码进行一些修改。在渲染场景时,我们需要将深度贴图传递给着色器,使用阴影贴图算法计算每个片段的阴影值。《OpenGL阴影指南》——在OpenGL中实现实时阴影的方法。最终,我们得到了实时阴影效果。

PixelLogic的博客 472

openGL阴影实现(软阴影)

openGL软阴影,消除伪影,和阴影锯齿

aoxuestudy的专栏 3042

OpenGL 阴影贴图实例

阴影计算的方法是,对于每个像素,我们将其从摄像机视角下的位置变换到光源视角下的位置,并计算该位置对应的深度值。如果该深度值小于深度纹理中对应位置存储的深度值,则该像素被遮挡,即产生阴影。接着,我们需要将渲染目标设置为帧缓冲对象,以便将深度信息存储到深度纹理中。接着,我们需要创建一个纹理对象,并为它设置纹理参数。现在我们已经生成了深度纹理,并将深度信息存储到该纹理中,我们需要进行第二次场景渲染,并使用深度纹理来计算阴影。现在我们已经创建了深度纹理,我们需要进行第一次场景渲染,并将深度信息存储到深度纹理中。

2301_79326588的博客 325

rk3568 OpenHarmony4.0如何使用中文输入法

问题:rk3568开发板 OpenHarmony4.0release系统自带输入法kikaInput软件只有英文输入,不含中文输入,不方便。本文基于rk3568开发板 OpenHarmony4.0release移植了laval社区发布的开发者手机中的搜狗输入法(测试版),实现了中文输入,为OpenHarmony4.0release的其他开发板中文输入提供参考。

Devlin_的博客 2912

LearnOpenGL学习笔记—高级光照 03(2):点阴影

LearnOpenGL学习笔记—高级光照 03(2):点阴影1 万向阴影贴图1.1 生成深度立方体贴图 本节对应官网学习内容:点阴影 1 万向阴影贴图 上个教程我们学到了如何使用阴影映射技术创建动态阴影。 效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。 所以它也叫定向阴影映射,深度(阴影)贴图生成自定向光的视角。 本节我们的焦点是在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。 这个技术叫做点光阴影,过去的名字是万向阴影贴图(omnidirectional shad

清清!的博客 792

Gazebo手册:【1】gazebo基本操作案例

欢迎来到初学者模块!本模块将引导您了解 Gazebo 的最基本功能。我们将构建一个简单的车辆来演示这些功能。这些教程适用于 Gazebo 新手和/或没有编程或 Linux 经验的人。 每个教程都建立在上一个教程的基础上,因此我们建议您按顺序学习这些教程。 一、何为Gazebo? Gazebo 是一款 3D 动态模拟器,能够准确有效地模拟复杂室内和室外环境中的机器人种群。虽然类似于游戏引擎,但 Gazebo 提供了更高保真度的物理模拟、一套传感器以及用于用户和程序的接口。 1............

gongdiwudu的专栏 2万+

LearnOpenGL学习笔记—高级光照 03(1):阴影映射

LearnOpenGL学习笔记—高级光照 03:阴影映射、点阴影0 前言1 知识:阴影映射2 知识:深度贴图2.1 知识:光源空间的变换2.2 知识:渲染至深度贴图3 深度贴图实践 0 前言 在用几何着色器做机器人爆破中出现了一个BUG,此时才发现并修复 (不过代码已经在发现后及早做了修改,以几何着色器那节的代码为准,并且之后的章节都没有再去弄爆破效果,所以除了那节以外没有影响) 详情可点击 几何着色器 的4.1节 1 知识:阴影映射 本节对应官网学习内容:阴影映射 关于知识部分,也可以结合着看当时学gam

清清!的博客 1297

现代软件开发技术 | 第1章:Spring基础

Spring是一个轻量级Java 开发框架,最早由Rod Johnson创建,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。

知识库搭建ing 455

生产环境中redis的SCAN命令如何替代KEYS命令?

本文介绍了使用SCAN命令替代KEYS的完整方案。SCAN通过游标分批迭代,避免KEYS命令遍历整个键空间导致的阻塞风险。文章详细说明了SCAN的基础语法、迭代流程和Java实现示例,强调必须关闭Cursor避免连接泄漏。针对生产环境,提供了COUNT参数调优、客户端去重、集群模式适配等优化建议,并列举了典型错误场景。SCAN通过分批次迭代和游标管理,可安全实现高性能键空间遍历。全文150字。

DN金猿的博客 179

LearnOpenGL——阴影映射

因为我们渲染的是深度贴图(黑白灰),所以颜色缓冲没啥用,所以我们将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE,告诉OpenGL我们不适用任何颜色数据进行渲染。图片每个斜坡代表深度贴图一个单独的纹理像素,有些在地板上面,有些在地板下面,这样我们所得到的阴影就有了差异。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中,发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。

weixin_70073176的博客 1294

[Python] 为什么你的脚本明明没报错,却什么也没发生?

Python脚本运行后没报错却没结果?这是许多开发者遇到的棘手问题。文章分析了六种常见原因:代码未进入目标逻辑分支、输出路径错误、中间数据为空、异常被吞掉、异步操作未完成、运行了错误脚本版本。建议通过"过程可视化"排查,如打印关键变量、检查工作目录、验证数据状态等。职场中更应关注结果验证而非单纯代码执行,确保脚本真正解决问题。建立结果验证意识是从"会写代码"到"稳定交付"的关键转变。

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 303

LearnOpenGL——点光源阴影笔记

为了把值渲染到深度立方体贴图,我们将需要3个着色器:顶点和像素着色器,以及一个它们之间的几何着色器。点光源发出的光线向四面八方扩散,对于ShaderCalculation函数,与普通的阴影计算不同,我们将fragment的光空间位置换成了fragment的位置,我们将使用一个方向向量来采样立方体深度贴图。我们创建了6个视图矩阵,把它们乘以投影矩阵,来得到6个不同的光空间变换矩阵。使用这些着色器渲染场景,立方体贴图附加的帧缓冲对象激活以后,你会得到一个完全填充的深度立方体贴图,以便于进行第二阶段的阴影计算。

weixin_70073176的博客 1120

专栏二十:如何选择你的单细胞亚群的分辨率--ROGUE

ROGUE这是继chooseR后第二个计算单细胞亚群分辨率的内容,其实关于代码的运行很简单,网上已经有不少教程,重复这些就没意思了,这里主要是结合探讨一下这个算法。

m0_58549466的博客 981

[ vulhub漏洞复现篇 ] Django SQL注入漏洞复现 CVE-2021-35042

[ vulhub漏洞复现篇 ] Django SQL注入漏洞复现 CVE-2021-35042 Django 组件存在 SQL 注入漏洞,该漏洞是由于对 QuerySet.order_by()中用户提供数据的过滤不足,攻击者可利用该漏洞在未授权的情况下,构造恶意数据执行 SQL 注入攻击,最终造成服务器敏感信息泄露。

qq_51577576的博客 3228

【实践LearnOpenGL项目】高级光照篇

本文详细介绍了OpenGL中三种图形渲染技术的实现方法:1. 阴影贴图技术:通过创建深度纹理和帧缓冲对象,从光源视角渲染场景,解决阴影伪影问题时采用PCF渐进过滤和动态阴影偏移。2. 点光源阴影技术:使用立方体贴图记录六个方向的深度信息,通过几何着色器实现单次多面渲染,并采用20次PCF采样实现柔和阴影效果。3. 法线贴图技术:利用TBN矩阵转换空间坐标,通过切线空间中的法线纹理增强表面细节表现。文章包含完整的代码实现和效果对比图,为图形渲染开发提供了实用参考。

qq_37378423的博客 1963

YOLOv12优化:原创自研 | 带有注意力机制的隐藏状态混合器的状态空间(HSM-SSD),实现暴力涨点 | CVPR2025 EfficientViM

可以将 带有注意力机制的隐藏状态混合器的状态空间(HSM-SSD)中的注意力机制换成专栏其他注意力机制 ,那么你就能实现独一无二的原创。

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 664

LearnOpenGL——高级光照(四)点光源阴影

本文介绍了点光源阴影的实现技术——万向阴影贴图。相比传统的定向阴影映射,该方法使用立方体贴图存储六个方向的深度信息,从而支持点光源的全方位阴影计算。关键技术包括:1)通过几何着色器一次性渲染六个面;2)创建90度视角的光空间变换矩阵;3)使用立方体贴图采样深度值进行阴影判断。实现过程分为深度贴图生成和正常渲染两个阶段,其中深度贴图阶段利用透视投影和六组视图矩阵将场景渲染到立方体贴图的各个面。这种方法在保持阴影质量的同时,显著提升了点光源阴影的渲染效率。

QQ805009795的博客 633

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

LearnOpenGL笔记——五、高级光照:“阴影”(“阴影映射”和“点阴影”)

五、高级光照:“阴影”(“阴影映射”和“点阴影”) 5.3.1 阴影映射 阴影还是比较不好实现的,因为当前实时渲染领域还没找到一种完美的阴影算法。目前有几种近似阴影技术,但它们都有自己的弱点和不足,这点我们必须要考虑到。 视频游戏中较多使用的一种技术是阴影贴图(shadow mapping),效果不错,而且相对容易实现。阴影贴图并不难以理解,性能也不会太低,而且非常容易扩展成更高级的算法(比如 Omnidirectional Shadow Maps和 Cascaded Shadow Maps)。 阴影映

weixin_44478658的博客 556

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

LearnOpenGL学习笔记2-Shaders

这一节介绍了基本的GLSL语法;着色器的输入与输出;全局变量在着色器的使用;颜色属性的添加;着色器类的封装。

LLLawrenceQ的博客 1875

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

LearnOpenGL学习笔记—PBR:理论

LearnOpenGL学习笔记—PBR1 理论1.1 引入1.2 微表面模型1.3 能量守恒1.4 反射方程1.4.1 辐射度量学1.4.2 辐射通量(Radiant Flux)1.4.3 立体角(Solid Angle)1.4.4 辐射强度(Radiant Intensity)1.4.5 入射辐照度(Irradiance)1.4.6 出射辐射度(Radiance)1.4.6 概念:双向反射分布函数(Bidirectional ReflectanceDistribution Function,BRDF)1.

清清!的博客 2880

LearnOpenGL——高级光照(三)阴影映射

阴影映射技术通过从光源视角生成深度贴图来实现真实感阴影效果。首先创建帧缓冲对象和深度纹理,以光源为视角渲染场景获得深度值。然后使用正交投影矩阵和视图矩阵构建光空间变换矩阵,将场景坐标转换到光源视角。在渲染阶段,先以光源视角生成深度贴图,再正常渲染场景时通过比较片段深度与深度贴图值判断是否处于阴影中。这种方法虽非完美,但性能较好,是实时渲染中常用的阴影实现方案。

QQ805009795的博客 704

LearnOpenGL学习笔记—入门05:Texture

小学期终于结束了,可以回来更新了Orz,总是拖拖拉拉的(瘫 0 前言 本节笔记对应的内容 纹理 在入门01中我们配置好了环境 在入门02中我们可以检测输入并出现一个有颜色的窗口 在入门03中我们初步学习了图形渲染管线,尝试用不同方法画出了三角形和四边形 在入门04(上)中我们学习了shader和GLSL的相关知识,并给图形加上了变换的颜色以及彩色。 在入门04(下)中我们建立自己的shader类,并能够从外部读取shader的内容。 在这一节,我们将会了解有关材质的内容 1 纹理简介 在之前的学习中,我们可

清清!的博客 1867

[learnOpenGL学习笔记_1] Macbook Pro下搭建learnOpenGL环境

Macbook Pro下搭建learnOpenGL环境 文章目录Macbook Pro下搭建learnOpenGL环境1. 创建一个新的XCode工程2. 搭建learnOpenGL环境2.1 获取GLFW2.2 配置GLAD3. 验证环境,生成一个窗口 learnOpenGL网站是学习openGL非常有用的网站,作为一个小白,为了方便后续回顾同时给大家提供借鉴,在此记录学习的过程。 learn...

同学小张的博客 856

LearnOpenGL学习笔记—模型加载02: Mesh

LearnOpenGL学习笔记—模型加载02: Mesh0 前言1 Mesh类1.1 Mesh.h1.2 Mesh.cpp1.2.1 构造函数1.2.2 初始化 0 前言 本节笔记对应官网学习内容:网格 在模型加载01中我们导入了assimp库。 通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。 我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。 我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一

清清!的博客 1393
上一篇: LearnOpenGL学习笔记——Gamma校正
下一篇: LearnOpenGL学习笔记——法线贴图
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值