OpenGL学习29——点阴影

点阴影(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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容