美文网首页
OpenGL学习29——点阴影

OpenGL学习29——点阴影

作者: 蓬篙人 | 来源:发表于2021-08-11 20:47 被阅读0次

点阴影(point shadow)

上一章节我们了解使用阴影映射创建动态阴影,但是只适合用于定向光源产生的阴影,因此也称为定向阴影映射(directional shadow mapping)。本章我们讨论如何在所有方向上生成动态阴影,这项技术特别适合点光源,因此也称为点阴影(point shadow),或更正式的名称叫做全向阴影映射(omnidirectional shadow mapping)

  • 全向阴影映射与定向阴影映射相似,都是先生成基于光源视角的深度图,然后基于片元位置从深度图采样,最后通过比较每个片元当前存储的深度值来判断是否处于阴影中,两者的主要区别就是所使用的深度图。
  • 全向阴影映射使用立方体贴图将整个场景渲染到立方体的各个面,并从这6个面中采样点光源周围环境的深度值。见下图:(图片取自书中
    点阴影

1. 生成深度立方体贴图

  • 创建一个环绕光源的深度立方体贴图的一种方式就是使用6个视矩阵分别渲染场景6次,每次将立方体贴图的不同面附加到帧缓冲区。代码看起来如下:
for (unsigned int i = 0; i < 6; i++)
{
    GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, face, depthCubemap, 0);
    BindViewMatrix(lightViewMatrices[i]);
    RenderScene();
}
  • 使用上述方法需要很多渲染操作调用,太过繁琐,本章我们采用另外一种方式:在几何着色器中使用一个小技巧来让我们用一次渲染调用完成立方体贴图的构建。(注意:采用几何着色器的方式不一定性能更好,具体哪种方法性能更优需根据渲染的场景,显卡型号等进行测试)
    1. 首先生成立方体贴图。
unsigned int depthCubemap;
glGenTextures(1, &depthCubemap);
    1. 为每个立方体贴图面指定一个2D深度值纹理图像。
const unsigned int SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
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);
}
    1. 设置立方体贴图纹理参数。
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
    1. 使用glFramebufferTexture函数将立方体贴图纹理附加为帧缓冲区的深度附件。
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
  • 与上一章节相似,阴影映射的两个阶段的伪代码如下:
// 第一阶段:渲染深度立方体贴图
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glClear(GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 第二阶段:使用深度立方体贴图渲染场景
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.1 基于光源视角转换

  • 设置好帧缓冲区和立方体贴图后,我们需要一种方法将场景的所有几何基元转换到光源6个方向上的光源空间。与阴影映射章节一样,我们需要一个光源空间的转换矩阵T,但是这一次立方体的每个面都需要一个。
  • 光源空间转换矩阵包含一个投影矩阵和一个视矩阵,对于每个转换矩阵我们使用相同的投影矩阵。
float aspect = (float)SHADOW_WIDTH / (float)SHADOW_HEIGHT;
float near = 1.0f;
float far = 25.0f;
glm::mat4 shadowProj = glm::perspective(glm::radians(90.0f), apsect, near, far);
  • 对于投影矩阵需要注意的一点是我们将视角角度设置为90.0f。这是为了保证视场正好大到足够填充立方体贴图的一个面,这样所有的面就能够沿着边缘对齐。
  • 每个方向我们使用相同的投影矩阵,但是对于视矩阵,我们需要使用glm::lookAt函数创建面向立方体贴图6个面的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)));

1.2 深度着色器

  • 要渲染深度值到立方体贴图,我们需要完整使用三种着色器。其中几何着色器负责将顶点坐标从世界空间转换到6个不同的光源空间。因此,顶点着色器只是将顶点坐标转换到世界空间并传递给几何着色器。顶点着色器如下:
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(aPos, 1.0);
}
  • 几何着色器使用内置变量gl_Layer来指定往立方体贴图的那个面输出基元。如果不管该变量,几何着色器像往常一样将数据传递到渲染管道的下一个阶段,但是如果我们更新该变量我们可以控制将每个基元渲染到立方体贴图的那个面。当然这需要有一个立方体贴图纹理附加到当前激活的帧缓冲区。几何着色器如下:
#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();
    }
}
  • 上一章我们使用一个空的片元着色器,让OpenGL自己决定深度图的深度值。这次我们自己计算最近片元位置与光源位置的线性距离作为深度值。片元着色器如下:
#version 330 core
in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void mian()
{
    // 获取片元与光源的距离
    float lightDiatance = length(FragPos.xyz, lightPos);
    // 除以far_plane,映射到[0;1]范围
    lightDiatance = lightDiatance / far_plane;
    // 写入深度值
    gl_FragDepth = lightDiatance;
}

2. 全向阴影映射

  • 渲染全向阴影的过程与定向阴影映射相似,只是这次我们需要绑定立方体贴图纹理并且将光源投影的远平面变量传递给着色器。伪代码如下:
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();
// ... 发送变量值到着色器
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// 绑定其他纹理
RenderScene();
  • 场景的顶点着色器和片元着色器与阴影映射章节的相似,差别在于我们现在使用方向矢量来采样深度值,因此不需要光源空间的片元位置。因此我们可以移除顶点着色器的FragPosLightSpace变量。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out VS_OUT
{
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
uniform mat4 lightSpaceMatrix;

void main()
{
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
    vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;
    vs_out.TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
  • 片元着色器主要改变在于阴影计算函数,因为现在我们需要从立方体贴图纹理而不是二维纹理采样深度值。下面我们逐步讨论函数的内容。首先我们需要从立方体贴图纹理检索深度值。
float ShadowCaculation(float fragPos)
{
    vec3 fragToLight = fragPos - lightPos;
    float closestDepth = texture(depthMap, fragToLight).r;
}
  • 将深度值从[0, 1]转换到[0, far_plane]。
closestDepth *= far_plane;
  • 检索当前片元的深度值,由前面我们计算深度值的方式,我们知道其实就是片元与光源之间的距离。
float currentDepth = length(fragToLight);
  • 计算阴影值并应用偏移消除阴影粉刺。
float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
  • 最后,完整的片元着色器如下:
#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec3 FragPos;
    vec3 Normal;
    vec2 TexCoords;
} fs_in;

uniform sampler2D diffuseTexture;
uniform samplerCube shadowMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

float ShadowCaculation(vec3 fragPos)
{
    vec3 fragToLight = fragPos - lightPos;
    float closestDepth = texture(shadowMap, fragToLight).r;
    closestDepth *= far_plane;
    float currentDepth = length(fragToLight);
    float bias = 0.05;
    float shadow = currentDepth - bias > 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(0.3);
    // ambient
    vec3 ambient = 0.3 * 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);
    float spec = 0.0;
    vec3 halfwayDir = normalize(lightDir + viewDir);
    spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);
    vec3 specular = spec * lightColor;
    // caculate shadow
    float shadow = ShadowCalculation(fs_in.FragPos);
    vec3 lighting = (ambient + (1.0 -shadow) * (diffuse + specular)) * color;

    FragColor = vec4(lighting, 1.0);
}
  • 渲染效果。


    全向阴影映射
  • 当程序渲染异常时,一般我们都会检查深度图是否正常构建。可视化深度缓冲区我们可以采用ShadowCaculation函数中的closestDepth作为片元输出。
vec3 fragToLight = fs_in.FragPos - lightPos;
float closestDepth = texture(shadowMap, fragToLight).r;
FragColor = vec4(vec3(closestDepth / far_plane), 1.0);
  • 深度立方体贴图。


    深度立方体贴图

3. PCF

边缘锯齿
  • 全向阴影映射与定向阴影映射都基于相同的准则,因此都存在依赖于分辨率的伪影(见上图)。我们可以采取与上一章相同的PCF过滤器来平滑边缘锯齿。在上一章PCF的基础上我们添加第三个维度,如下:
float shadow = 0.0;
float bias = 0.05;
float samples = 4.0;
float offset = 0.1;
for(float x = -offset; x < offset; x += offset / (samples * 0.5))
{
    for(float y = -offset; y < offset; y += offset / (samples * 0.5))
    {
        for(float z = -offset; z < offset; z += offset / (samples * 0.5))
        {
            float closestDepth = texture(depthMap, fragToLight + vec3(x, y, z)).r;
            closestDepth *= far_plane;
            if(currentDepth - bias > closestDepth)
                shadow += 1.0;
        }
    }
}
shadow /= (samples * samples * samples);
  • 渲染效果如下:


    PCF
    PCF拉近效果
  • 上述PCF使用四个采样点,这样每个片元需要进行64次采样,增加了很多计算。而且这些采样很多都是冗余的,因为这里面很多与原来采样的方向矢量十分接近。但是我们也很难区分哪些子采样是冗余的,有一个小技巧就是我们使用一个偏移数组来区分采样方向矢量,让不同子采样指向不同的方向。这样我们就可以降低子采样的数量。下面是一个20个元素的偏移数组:
vec3 samplesOffsetDirections[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)
);
  • 使用上面的偏移数组,我们可以调整PCF算法,采用固定数量的子采样来对立方体贴图进行采样。
float shadow = 0.0;
float bias = 0.05;
int samples = 20.0;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0;i < 20; ++i)
{
    float closestDepth = texture(depthMap, fragToLight + samplesOffsetDirections[i] * diskRadius).r;
    closestDepth *= far_plane;
    if(currentDepth - bias > closestDepth)
        shadow += 1.0;
}
shadow /= float(samples);
  • 另外一个技巧是我们可以根据观察者与片元的距离调整diskRadius的大小,这样可以让视角拉远时阴影更柔和,拉近时则更锐化。
float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;
  • 渲染效果。


    偏移数组PCF

相关文章

  • OpenGL学习29——点阴影

    点阴影(point shadow) 上一章节我们了解使用阴影映射创建动态阴影,但是只适合用于定向光源产生的阴影,因...

  • OpenGL阴影

    几种绘制阴影的方法 在OpenGL中,比较常见的绘制阴影的方法有:shadow mapping,shadow vo...

  • OpenGL学习28——阴影映射

    阴影映射(Shadow Mapping) 阴影是遮挡导致光线缺失造成的。阴影能够让我们的场景变得更加真实,也能让我...

  • OpenGL基本图元

    OpenGL点/线 今天我们将从更底层更基础的角度来详细学习OpenGL图元渲染。点,是最简单的图像。每个特定的顶...

  • 27.opengl高级光照-点光源阴影

    一、原理 接上一篇继续,实际中的光源几乎不会是平行光,点光比较多。点光的阴影生成原理和阴影映射基本相同,区别是阴影...

  • OpenGL ES 3.0 - 顶点着色器

    顶点着色器 如图展示OpenGL ES 3.0可编程管线,有阴影的方框表示OpenGL ES 3.0中的可编程阶段...

  • Android OpenGL 基本环境,绘制简易图形

    学习Android 平台 OpenGL ES API,了解 OpenGL 开发的基本流程,使用 OpenGL 绘制...

  • OpenGL ES初步了解

    前面学习了OpenGL相关知识,下面过渡到OpenGL ES的学习中,本章主要是对OpenGL ES的初步了解。 ...

  • iOS开发之OpenGL ES(一) —— 初探

    在学习完OpenGL的相关基础后,开始了OpenGL ES的学习,从了解OpenGL ES开始。 什么是OpenG...

  • OpenGL初探

    以下为个人学习过程中,记录的OpenGL的知识点。详情请咨询OpenGL Wiki。 现有图形API简介 Open...

网友评论

      本文标题:OpenGL学习29——点阴影

      本文链接:https://www.haomeiwen.com/subject/bwqhbltx.html