您当前的位置: 首页 > 

Jave.Lin

暂无认证

  • 4浏览

    0关注

    704博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

OpenGL - 阴影映射 - Tutorial 16 : Shadow mapping

Jave.Lin 发布时间:2020-04-10 10:58:24 ,浏览量:4

文章目录
  • Basic shadowmap - shadow map的基础知识
    • Rendering the shadow map - 渲染shadow map
      • Setting up the rendertarget and the MVP matrix - 设置渲染目标和MVP矩阵
      • The shaders - Shader着色器
      • Result - 渲染结果
    • Using the shadow map - 使用shadow map
      • Basic shader - 简单的shader
      • Result - Shadow acne - 渲染结果的阴影粉刺
  • Problems - 问题
    • Shadow acne - 阴影粉刺
    • Peter Panning - 彼得·潘宁
    • Aliasing - 锯齿
      • PCF - 靠近边缘百分比滤波
      • Poisson Sampling - 泊松采样
      • Stratified Poisson Sampling - 分层泊松采样
  • Going further - 进一步探索
    • Early bailing - 提前判定
    • Spot lights - 聚光灯
    • Point lights - 点光源
    • Combination of several lights - 多组光源的使用
    • Automatic light frustum - 自适应的光源视锥体
    • Exponential shadow maps - 指数分布阴影图
    • Light-space perspective Shadow Maps - 光源空间的透视Shadow Maps
    • Cascaded shadow maps - 级联阴影图
    • Conclusion - 总结

最近在学习数学的一些基础知识,发现内容超级多,有时学累了,还是看看别的,再继续学习,效果会好一些,好了,今天就学习一下OpenGL中实现Shadow mapping的内容,翻译一篇文章。

能力有限,如有错误,欢迎指正。

原文:Tutorial 16 : Shadow mapping

(翻译到一般,不小心刷新了原文网页,然后刚好原文网站维护,一直打不开,404之类的,过了大半天,我每隔一小时刷新一下,翻译过程也是断断续续的,终于2020.04.09 18:22又可以打开了,那么继续翻译吧)

在Tutorial 15我们学习了如何创建包含静态光的lightmaps。它能生成非常好的阴影,但对于会动的对象是没用的。

Shadow map是当今(2016)用于创建动态阴影的方法。还好的是它的工作方式是非常的简单的。不好的是,想要它的效果处理好是很难的。

在此教程中,我们将介绍基础的算法知识、缺点,以及实现上的一些技巧来得到更好的效果。以前(2012)要编写实现shadow maps还是一个需要深入研究的话题,现在我们将告诉你如何实现,并根据你想要的效果进一步优化你的shadowmap。

Basic shadowmap - shadow map的基础知识

shadow map的基础算法包含两个pass。首先,在光源的视角下渲染场景。仅计算每个片段的深度。下一步是,与平常一样的渲染场景,但会多了一步去测试当前的片段是否在阴影中。

“判断在阴影”的检测是非常的简单的。如果当前的渲染片段比shadow map上对应的片段的深度距离还要远,这意味着场景中还有其他更靠近光源的对象挡住了。换句话说,就是当前的片段处于阴影中。

如下图,可能会帮助你理解原理: 在这里插入图片描述

Rendering the shadow map - 渲染shadow map

在次教程中,我们只考虑方向光 - 光源假设是非常远的,并且光的射线都假设是平行的。所以,完成shadow map的渲染是使用正交投影矩阵的。正交矩阵就像一个透视矩阵一样,但没有透视效果 - 无论对象的远近,看起来都是一样的。

Setting up the rendertarget and the MVP matrix - 设置渲染目标和MVP矩阵

在Tutorial 14你知道如何渲染场景到一张纹理,让后续的shader可以访问到。

这里我们使用1024x1024 16位的深度格式的纹理来包含shadow map纹理。16位已经够用于shadow map了。你可以随意去更改这时配置值。注意我们使用的是一个深度纹理,而不是深度渲染缓存,因为我们后续还要对它采样。

// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.
// 创建framebuffer帧缓存对象,并重新编组0个或1个,或是更多的纹理,与0个或1个深度缓存
 GLuint FramebufferName = 0;
 glGenFramebuffers(1, &FramebufferName);
 glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);

 // Depth texture. Slower than a depth buffer, but you can sample it later in your shader
 // 深度纹理。比深度缓存慢一些,但可以用于后续的shader来采样
 GLuint depthTexture;
 glGenTextures(1, &depthTexture);
 glBindTexture(GL_TEXTURE_2D, depthTexture);
 glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT16, 1024, 1024, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

 glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0);

 glDrawBuffer(GL_NONE); // No color buffer is drawn to.
 // 不需要绘制颜色缓存

 // Always check that our framebuffer is ok
 // 总是需要检测framebuffer是OK的
 if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
 return false;

MVP矩阵用于渲染场景用的,它是在光源位置的视角上计算的,如下:

  • 投影矩阵(Project Matrix)是一个正交矩阵,它将包含所有的东西在一个轴对齐的盒子中,X,Y,Z轴分别是:(-10,10),(-10,10),(-10,20)的大小。这些值的设置将会让我们的整个场景总是可见的范围;这些内容在后续的章节会有进一步说明。
  • 视角矩阵(View Matrix)用于旋转整个世界到相机空间,光源的方向就是的-Z(你可以重新阅读Tutorial 3)
  • 世界矩阵(Model Matrix/World Matrix)根据你想要的来设置就好。
glm::vec3 lightInvDir = glm::vec3(0.5f,2,2);

 // Compute the MVP matrix from the light's point of view
 // 从光源的视角来计算MVP矩阵
 glm::mat4 depthProjectionMatrix = glm::ortho(-10,10,-10,10,-10,20);
 glm::mat4 depthViewMatrix = glm::lookAt(lightInvDir, glm::vec3(0,0,0), glm::vec3(0,1,0));
 glm::mat4 depthModelMatrix = glm::mat4(1.0);
 glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrix * depthModelMatrix;

 // Send our transformation to the currently bound shader,
 // in the "MVP" uniform
 // 给当前的绑定使用的shader设置uniform MVP
 glUniformMatrix4fv(depthMatrixID, 1, GL_FALSE, &depthMVP[0][0])
The shaders - Shader着色器

这个pass的shader非常简单。顶点着色器就只是简单的计算顶点位置到齐次坐标就好了:

#version 330 core

// Input vertex data, different for all executions of this shader.
// 输入的顶点数据,每个执行的shader都不同
layout(location = 0) in vec3 vertexPosition_modelspace;

// Values that stay constant for the whole mesh.
// 整个mesh渲染时都是保持不变的常量
uniform mat4 depthMVP;

void main(){
 gl_Position =  depthMVP * vec4(vertexPosition_modelspace,1);
}

片段着色器就更加简单:将片段的深度写入到布局限定符为0的寄存器上(例子中是我们的深度纹理中)。

#version 330 core

// Ouput data
// 输出的数据
layout(location = 0) out float fragmentdepth;

void main(){
    // Not really needed, OpenGL does it anyway
    // 其实不需要处理,OpenGL不论如何都会处理的
    fragmentdepth = gl_FragCoord.z;
}

渲染shadow map通常是比普通的渲染快两倍的,因为仅仅写入的是低精度的深度,而不是深度与颜色;处于GPU中内存带宽通常是最大的性能状态。

Result - 渲染结果

渲染结果的纹理看起来是这样的: 在这里插入图片描述

灰暗的颜色意味着一个很小的z值;因此,墙的右上角是比较近于相机的。相反,白色意味着z=1(在齐次坐标),就是非常远的片段。

Using the shadow map - 使用shadow map Basic shader - 简单的shader

现在我们回到shader部分。每个我们计算的片段,我都必须测试它处于shadow map纹理的“背后”与否。

为了做到这点,我们需要计算当前片段的位置处于在我们创建shadow map时的空间下的位置。所以我们需要变换一次处理:将MVP矩阵与depthMVP矩阵变换一次。

这里有一些技巧。顶点的坐标在应用depthMVP矩阵变换后我们将得到齐次坐标,它的作为值范围都在[-1,1]之间的;但是纹理的采样都范围都必须是[0,1]范围的。

就这个例子而言,一个片段要处于屏幕中间将会是(0,0)的齐次坐标;但采样纹理的中间的话,UV坐标是(0.5,0.5)。

这个调整可以修正直接渲染时得到的片段着色器中的片段坐标,但可以乘以下面的矩阵可以更高效的处理,就是简单的除以了一个2(就是矩阵对角上的系数:将[-1,1]->变化为[-0.5,0.5])然后在平移他们(矩阵中最后的一行,将:[-0.5,0.5]->变化为[0,1])

(译者jave.lin:如果还看不懂我就简单的描述一下:就是对应 ( ( − 1 , 1 ) × 0.5 ) + 0.5 = ( 0 , 1 ) ((-1,1)\times 0.5)+0.5=(0,1) ((−1,1)×0.5)+0.5=(0,1),不过它是在CPU层对depthMVP中处理的,在片段着色器就不用每个片段都再执行一次这个转换了,这就是合理使用矩阵复合运算,并将这些公共部分提取到CPU层的威力)

glm::mat4 biasMatrix(
0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.5, 0.5, 0.5, 1.0
);
glm::mat4 depthBiasMVP = biasMatrix*depthMVP;

现在我们可以编写顶点着色器了。他还是和我们之前的一样,但我们将有2个输出数据而不是1个了:

  • gl_Position 是顶点坐标处于camera下的坐标。
  • ShadowCoord 是顶点坐标处于light space下的坐标(光源空间下)
// Output position of the vertex, in clip space : MVP * position
// 输出顶点的坐标,在clip space下:MVP * position
gl_Position =  MVP * vec4(vertexPosition_modelspace,1);

// Same, but with the light's view matrix
// 同样的,但这是光源视角矩阵下的
ShadowCoord = DepthBiasMVP * vec4(vertexPosition_modelspace,1);

片段着色器是非常简单的:

  • texture(shadowMap, ShadowCoord.xy).z 是光源与最近的深度遮挡对象的距离。
  • ShadowCoord.z 是光源与当前渲染片段的距离。

所以,如果当前渲染的片段比shadowmap中深度遮挡对象的距离值大的话,就意味着处于阴影中(或是说:有其他的对象比当前渲染的对象更加靠近与光源):

float visibility = 1.0;
if ( texture( shadowMap, ShadowCoord.xy ).z   仅绘制背面三角

而正常的渲染场景时(剔除背面)

glCullFace(GL_BACK); // Cull back-facing triangles -> draw only front-facing triangles
// 剔除背面三角 -> 仅绘制正面三角

这种方式的话,就需要用到bias

Peter Panning - 彼得·潘宁

(译者jave.lin:为何叫:Peter Panning,就是小时候看的:《小飞侠》国外卡通片的主角名字,下面会讲到,就是一个术语 在这里插入图片描述)

现在没有阴影粉刺了,但在地板仍然还是有一些错误的着色,导致墙体像是漂浮起来似的(因此术语叫:“Peter Panning”《小飞侠》国外卡通片的主角名字)。实际上,添加了bias偏移后会让结果更糟糕。 在这里插入图片描述 这个问题非常容易修复:避免太薄的几何体。这有两个优点:

  • 第一:解决了 Peter Panning的问题:几何体厚度比你的bias大,就好了。
  • 第二:你可以开回剔除背面来渲染lightmap(或是叫shadowmap),因为现在,有另一个面向光源的多边形了,这个多边形就可以挡住另一面,这就不需要开启背面剔除来渲染了。

(我不知道是我对原文这两点的翻译理解有误,还是这两点的描述本身是很有问题的:第一点,bias是为了处理一个应该全亮的表面出现了acne粉刺的问题,而不是为了解决墙体与地板的看似腾空的问题。第二点,使用剔除正面的方式来绘制shadow map,就可以不使用bias的方式来偏移当前绘制的片段深度来消除acne,因为绘制几何体的背面来写入shadow map后,shadow map的深度值通常就会比正面的表面要大,只要几何体的厚度比分辨率导致失真的深度差大的话,就可以不使用bias来偏移了。)

缺点是,你需要绘制更多的三角形了 在这里插入图片描述

Aliasing - 锯齿

前面用过了两个技巧,但现在我们还是注意到阴影的边界有锯齿。话句话说,就是一个像素是白色的,然后旁边的另一个是黑色的,它们之间没有平滑过渡。 在这里插入图片描述

PCF - 靠近边缘百分比滤波

(全称是:Percentage Closer Filter) 最简单改进的方式就是调整shadowmap的采样器sampler2DShadow。然后当你每次对shadowmap进行采样时,其实硬件会采样到附近的纹素,然后比较它们数据,最后使用bilinear滤波后返回[0,1]之间的浮点数。

例如,0.5意味着采样了2个是在阴影里的,和2个是在光照里的。

注意这与depth map中的单采样不同!比较总是范围true或false;PCF返回的是4个"true or false"的插值。 在这里插入图片描述 如你所看到的,阴影的边界平滑了,但还是能看到shadowmap中的纹素块。

Poisson Sampling - 泊松采样

一个简单方式来处理就是采样N次shadowmap,而不是采样一次。使用PCF滤波组合,这将会得到很好的结果,就算是少量的N次。这里的代码是采样4次:

for (int i=0;i            
关注
打赏
1664331872
查看更多评论
0.2462s