零基础学习OpenGL(十二)--阴影(一) 阴影映射

       阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。

       阴影的实现:

       阴影映射:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。我们希望得到射线第一次击中的那个物体,然后用这个最近点和射线上其他点进行对比。然后我们将测试一下看看射线上的其他点是否比最近点更远,如果是的话,这个点就在阴影中。对从光源发出的射线上的成千上万个点进行遍历是个极端消耗性能的举措,实时渲染上基本不可取。我们可以采取相似举措,不用投射出光的射线。我们所使用的是非常熟悉的东西:深度缓冲。

        深度测试时已经计算过摄像机视角下,片元的深度值。可以把这些结果储存在纹理中(显示了可以看到的第一个片元的深度值),这样以后就可以对深度值采样。

       eg:渲染一个点P处的片元,需要决定它是否在阴影中。我们先得使用T变换把P变换到光源的坐标空间里。既然点P是从光的透视图中看到的,它的z坐标就对应于它的深度,例子中这个值是0.9。使用点P在光源的坐标空间的坐标,我们可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C,最近的深度是0.4。因为索引深度贴图的结果是一个小于点P的深度,我们可以断定P被挡住了,它在阴影中了。

       深度贴图:第一步我们需要生成一张深度贴图。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。把纹理格式指定为GL_DEPTH_COMPONENT。我们还要把纹理的高宽设置为1024:这是深度贴图的解析度。合理配置将深度值渲染到纹理的帧缓冲。

       为渲染的深度贴图创建一个帧缓冲对象:

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

       把生成的深度纹理作为帧缓冲的深度缓冲:

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

       生成深度贴图:

// 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();

       第二步:光源空间的变化,需要把点变换到光源空间。这里光源是平行光,使用正交投影。

glm::mat4 lightSpaceMatrix = lightProjection * lightView;//将每个世界空间坐标变换到光源处所见到的那个空间

        这个lightSpaceMatrix正是前面我们称为TT的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,我们就能像往常那样渲染场景了。

       为了提升性能,使用一个与之不同但更为简单的着色器来渲染出深度贴图。

顶点着色器:

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

}

片元着色器:

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

渲染阴影:

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

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

}

       像素着色器使用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)

{

     // 执行透视除法

     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;

}

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

}

       阴影失真:因为阴影贴图受限于解析度,在距离光源比较远的情况下,多个片元可能从深度贴图的同一个值中去采样。可以用一个叫阴影偏移(所有采样点都获得了比表面深度更小的深度值)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了。

float bias = 0.005;

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

有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:

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

        悬浮:使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移。这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上。可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling),OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。

       多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边。解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来。

参考自https://learnopengl-cn.github.io

       

猜你喜欢

转载自blog.csdn.net/jfy307596479/article/details/84752101
今日推荐