计算机图形学(OPENGL):点阴影

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

点阴影

  上一章我们使用阴影贴图创建了动态阴影,但我们使用的方式只适合方向光源。这里我们将讨论全方位阴影的创建方式,即适用于点光源的阴影,也可以称为全方位阴影贴图。
  这一技术和单方向阴影贴图很相似:我们从灯光的视角生成深度贴图,根据当前片段的位置采样深度贴图,并比较当前片段深度和存储在深度贴图中的深度来判断当前片段是否在阴影中。而单方位和全方位的区别在于我们使用的深度贴图不一样。

  如果要针对点光源渲染一张深度贴图,简单的一张2D贴图不会起作用,还记得立方体贴图吗?包含6个不同方位的纹理的立方体贴图可以很好的解决这一问题。

  生成的深度立方体贴图接着送往片元着色器,通过一个方向向量采样立方体贴图来获取最近深度点。

生成深度立方体贴图

  为了创建一个深度立方体贴图,我们需要渲染场景6次。其中一种方法时通过6个不同的view矩阵渲染场景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();  
}

  但这种方式开销很大,我们会使用一种替代的方法来建立深度立方体贴图,我们可以可以在几何着色器中使用一点技巧来保证只用渲染一次。
  首先,创建一个立方体贴图对象:

unsigned int depthCubemap;
glGenTextures(1, &depthCubemap);

  接着为立方体贴图的每个面生成一张深度纹理:

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

  接着设置纹理的映射方式和滤镜:

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

  我们将立方体贴图作为1个附加项附加到自定义的帧缓冲上:

glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthCubemap, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebuffer(GL_FRAMEBUFFER,0);

  注意我们要使用glDrawBuffer和glReadBuffer来保证不写入和使用颜色缓冲。
  渲染全方位阴影贴图有两个步骤,一是先渲染出深度图,二是使用深度图渲染正常场景,这和但方位阴影贴图没区别:

// 1. first render to depth cubemap
glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT);
glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO);
    glClear(GL_DEPTH_BUFFER_BIT);
    ConfigureShaderAndMatrices();
    RenderScene();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 2. then render scene as normal with shadow mapping (using depth cubemap)
glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
ConfigureShaderAndMatrices();
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
RenderScene();

灯光空间转换

  为了渲染深度图,我们需要将所有的场景的几何体转化到6个不同的灯光空间中。每个灯光空间的转换矩阵包含一个投影矩阵和一个视图矩阵。针对投影矩阵我们使用一个透视投影矩阵:

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), aspect, near, far); 

  注意我们将透视角设为了90度,这样可以保证覆盖范围足够大,以保证立方体贴图的每个面都在投影范围内。
  接着我们需要创建6个不同的view矩阵,我们使用glm::lookAt针对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));

着色器

  这次我们要创建3个着色器来渲染深度图,顶点、几何和片元。
  几何着色器的任务是将所有世界空间的顶点转化到6个不同的灯光空间,因此,我们只需要在顶点着色器中将顶点转化到世界空间即可:

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(aPos, 1.0);
}  

  几何着色器将每个三角形的三个顶点作为输入,并定义一个包含6个灯光空间变换矩阵的数组。
  几何着色器中的内建变量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();
    }
}

  这个几何着色器中,我们输入一个三角形,并在这个三角形的举出上输出6个三角形。在main方法中,我们遍历立方体贴图的6个面,并将每个面的序号存储在gl_Layer中。接着我们将每个三角形变换到立方体贴图6个面对应的灯光空间中,FragPos代表每个顶点的位置,并将这个变量与变换矩阵相乘得到灯光空间的位置。我们也将这个FragPos传入片元着色器来计算深度值。
  最后,在片元着色器中,我们这样做:

#version 330 core
in vec4 FragPos;

uniform vec3 lightPos;
uniform float far_plane;

void main()
{
    // 获取片段和光源位置的距离
    float lightDistance = length(FragPos.xyz - lightPos);
    
    // 通过和远平面的值相乘将距离映射到[0, 1]之间
    lightDistance = lightDistance / far_plane;
    
    // 将这一距离作为当前片段的深度值。
    gl_FragDepth = lightDistance;
}  

  这个片元着色器将几何着色器计算的的FragPos作为输入,同时uniform定义灯光位置和远平面。我们获取灯光和片段之间的距离,并将这个距离映射到[0,1]作为片段从某一面的光源方向观察的深度值。

全方位深度贴图

  做好这些准备后,我们开始渲染这张深度立方体贴图。过程和单方向深度贴图很相似,只是要将绑定的纹理类型换成立方体贴图:

glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shader.use();  
// ... send uniforms to shader (including light's far_plane value)
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap);
// ... bind other textures
RenderScene();

  接下来使用渲染好的深度图渲染我们的带阴影的场景。顶点着色器和片元着色器没有太大的改变,只是在片元着色器中我们不需要使用灯光空间的片段位置了,我们通过一个方向向量来采样深度值。
  在顶点着色器中我们不再需要将顶点转移到灯光空间:


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

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

  片元着色器如下:

#version 330 core
out vec4 FragColor;

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

uniform sampler2D diffuseTexture;
uniform samplerCube depthMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

uniform float far_plane;

float ShadowCalculation(vec3 fragPos)
{
    [...]
}

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);
    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 = ShadowCalculation(fs_in.FragPos);                      
    vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;    
    
    FragColor = vec4(lighting, 1.0);
}  

  片元着色器的区别在于我们定义了立方体贴图类型的深度图采样器,同时计算阴影的方法的参数传入当前片段位置,不使用灯光空间位置。
  在ShadowCalculation中,我们首先检索立方体贴图的深度值,我们像之前存储深度值到深度图中一样操作,当前片段位置减去灯光位置的向量作为采样的方向向量进行深度值采样:

float ShadowCalculation(vec3 fragPos)
{
    vec3 fragToLight = fragPos - lightPos; 
    float closestDepth = texture(depthMap, fragToLight).r;
}  

  通过方向向量采样立方体贴图的值不需要使用单位向量,所以这样就可以。
  结果的closestDepth的范围位于[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; 

  完整的ShadowCalculation方法如下:

float ShadowCalculation(vec3 fragPos)
{
    // get vector between fragment position and light position
    vec3 fragToLight = fragPos - lightPos;
    // use the light to fragment vector to sample from the depth map    
    float closestDepth = texture(depthMap, fragToLight).r;
    // it is currently in linear range between [0,1]. Re-transform back to original value
    closestDepth *= far_plane;
    // now get current linear depth as the length between the fragment and light position
    float currentDepth = length(fragToLight);
    // now test for shadows
    float bias = 0.05; 
    float shadow = currentDepth -  bias > closestDepth ? 1.0 : 0.0;

    return shadow;
}  

  最后,(这里使用了几个平面来构成室内的模型)运行结果如下:


  这里给出原文代码参考:Code

观察立方体贴图深度缓冲

  我们可以使用上面计算的closest值来观察一下深度值的分布:

FragColor = vec4(vec3(closestDepth / far_plane), 1.0);  

  结果会是这样:


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;   // Undo mapping [0;1]
            if(currentDepth - bias > closestDepth)
                shadow += 1.0;
        }
    }
}
shadow /= (samples * samples * samples);

  最后结果如下:



  然而,上述的代码中,我们对每一维度都采样了4次,总共就是64次,太多了!
  这些采样中大多数都是多余的,它们的采样向量很靠近原来的方向向量,然而只有与原来的方向向量垂直才能发挥更大的作用。然而,识别出哪些方向向量是多余的会很困难,有一个方法是粗略的给出一系列偏移方向来采样,这些方向完全不在同一方向,下面是20个偏移方向向量:

vec3 sampleOffsetDirections[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)
);   

  我们可以使用这些向量来更新方法:

float shadow = 0.0;
float bias   = 0.15;
int samples  = 20;
float viewDistance = length(viewPos - fragPos);
float diskRadius = 0.05;
for(int i = 0; i < samples; ++i)
{
    float closestDepth = texture(depthMap, fragToLight + sampleOffsetDirections[i] * diskRadius).r;
    closestDepth *= far_plane;   // Undo mapping [0;1]
    if(currentDepth - bias > closestDepth)
        shadow += 1.0;
}
shadow /= float(samples);  

  我们使用diskRadius来缩放这些偏移,环绕着原来的fragToLight进行采样。
  另一个技巧是我们可以基于观察者到片段的距离改变diskRadius,让阴影在远处更柔和,在近处更尖锐:

float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0; 

  结果如下:


  这里给出原文代码参考:Code
  需要注意的是,我们这里使用几何着色器来帮助渲染立方体深度贴图的方式并不一定是最好的,这需要针对实际情况而定,有时候反而是单独渲染立方体贴图的每个面更好,所以说如果对性能有很大的要求,可以测试这两种方式再进行选择。
  最后,贴图原文地址供参考:https://learnopengl.com/Advanced-Lighting/Shadows/Point-Shadows

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

推荐阅读更多精彩内容