- 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上对应的片段的深度距离还要远,这意味着场景中还有其他更靠近光源的对象挡住了。换句话说,就是当前的片段处于阴影中。
如下图,可能会帮助你理解原理:
在次教程中,我们只考虑方向光 - 光源假设是非常远的,并且光的射线都假设是平行的。所以,完成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
。
(译者jave.lin:为何叫:Peter Panning,就是小时候看的:《小飞侠》国外卡通片的主角名字,下面会讲到,就是一个术语 )
现在没有阴影粉刺了,但在地板仍然还是有一些错误的着色,导致墙体像是漂浮起来似的(因此术语叫:“Peter Panning”《小飞侠》国外卡通片的主角名字)。实际上,添加了bias
偏移后会让结果更糟糕。 这个问题非常容易修复:避免太薄的几何体。这有两个优点:
- 第一:解决了 Peter Panning的问题:几何体厚度比你的
bias
大,就好了。 - 第二:你可以开回剔除背面来渲染lightmap(或是叫shadowmap),因为现在,有另一个面向光源的多边形了,这个多边形就可以挡住另一面,这就不需要开启背面剔除来渲染了。
(我不知道是我对原文这两点的翻译理解有误,还是这两点的描述本身是很有问题的:第一点,bias是为了处理一个应该全亮的表面出现了acne粉刺的问题,而不是为了解决墙体与地板的看似腾空的问题。第二点,使用剔除正面的方式来绘制shadow map,就可以不使用bias的方式来偏移当前绘制的片段深度来消除acne,因为绘制几何体的背面来写入shadow map后,shadow map的深度值通常就会比正面的表面要大,只要几何体的厚度比分辨率导致失真的深度差大的话,就可以不使用bias来偏移了。)
缺点是,你需要绘制更多的三角形了
前面用过了两个技巧,但现在我们还是注意到阴影的边界有锯齿。话句话说,就是一个像素是白色的,然后旁边的另一个是黑色的,它们之间没有平滑过渡。
(全称是:Percentage Closer Filter) 最简单改进的方式就是调整shadowmap的采样器sampler2DShadow
。然后当你每次对shadowmap进行采样时,其实硬件会采样到附近的纹素,然后比较它们数据,最后使用bilinear滤波后返回[0,1]之间的浮点数。
例如,0.5意味着采样了2个是在阴影里的,和2个是在光照里的。
注意这与depth map中的单采样不同!比较总是范围true或false;PCF返回的是4个"true or false"的插值。 如你所看到的,阴影的边界平滑了,但还是能看到shadowmap中的纹素块。
一个简单方式来处理就是采样N次shadowmap,而不是采样一次。使用PCF滤波组合,这将会得到很好的结果,就算是少量的N次。这里的代码是采样4次:
for (int i=0;i
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?