OpenGL深入探索——阴影贴图(一)

举报
ShaderJoy 发表于 2021/12/30 02:21:05 2021/12/30
【摘要】 转载自:第二十三课 阴影贴图(一) 背景 阴影和光是紧密联系在一起的,因为如果你想要产生一个阴影就必须要光。有许多的技术可以生成阴影,在接下来的两个章节中我们将学习其中的一种比较基础简单的技术-阴影贴图。 当光栅化的时候,你会问这个像素是否位于阴影中?让我们换个问法,从光源到像素的路径是否中间经过其他物体?如果是,这个像素可能位于阴...

转载自:第二十三课 阴影贴图(一)

背景
阴影和光是紧密联系在一起的,因为如果你想要产生一个阴影就必须要光。有许多的技术可以生成阴影,在接下来的两个章节中我们将学习其中的一种比较基础简单的技术-阴影贴图。

当光栅化的时候,你会问这个像素是否位于阴影中?让我们换个问法,从光源到像素的路径是否中间经过其他物体?如果是,这个像素可能位于阴影中(假定其他的物体不透明),如果不是,则像素不位于阴影中。某种程度上这个问题和我们之前章节问的问题相似,如何确定当两个物体覆盖彼此时,我们看到的是比较近的那个。如果我们把相机放在光源的位置,那么两个问题变成一个。我们希望在深度测试中失败的像素处于阴影中。只有在在深度测试中获胜的像素受到光的照射。这些像素都是直接和光源接触的,其间没有任何东西会遮蔽它们。简单的说,这是在阴影贴图背后的原理。

看似深度测试可以帮助我们探测一个像素是否位于阴影中,但是还有一个问题:相机和光源不总位于同一个地方。深度测试通常用于解决从相机视口看物体是否可见的问题。所以当光源处于远处的时候,我们如何利用深度测试来进行阴影测试?解决方案是渲染场景两次。第一次从光源的角度。这次渲染过程的结果没有被存储到颜色缓冲区中。相反,离光源最近的深度值被渲染进入由应用程序创建的(而不是由 GLUT 自动生成的)深度缓冲区。在第二个过程则是像以前一样以相机为视口渲染场景。我们创建的深度缓冲区被绑定到片元着色器以便读取。对于每一个像素我们从这个深度缓冲区中取出相应的深度值(准确的说是取出当前像素到光源的路径上离光源最近的那个片元的深度值),同时我们也计算这个像素到光源的距离。有时候这两个值是相等的。这种情况说明这个像素与光源最近,因此它的深度值才会被写进深度缓冲区。如果这种情况发生,这个像素就被认为处于光照中并和往常一样计算它的颜色。如果这两个值是不相同,这意味着从光源看这个像素时有其他像素遮挡了它。这种情况下我们在颜色计算中增加阴影因子来模仿阴影效果。看看下面这幅图:


我们的场景由两个对象组成——表面和立方体。光源是位于左上角并且指向立方体。在第一个渲染过程,我们以光源位置为视口将深度信息渲染到深度缓冲区中。单看 A,B,C 这 3 个点。当 B 被渲染时,它的深度值进入深度缓冲区。因为在 B 和光源之间没有其他的东西。我们默认它是那条线上离光源最近的点。然而当 A 和 C 被渲染的时候,它们在深度缓冲区的同一个点进行比较。两个点都在同一条来自光源的直线上,所以在透视投影后,光栅器发现这两个点需要去往屏幕上的同一个像素。这就是深度测试,最后 C 点“赢”了,则 C 点的深度值被写入了深度缓存中。

在第二个渲染过程,我们以相机为视口渲染表面和立方体。我们在着色器中除了为每个像素做一些计算,我们还计算从光源到像素之间的距离,并和在深度缓冲区中对应的深度值进行比较。当我们光栅化 B 点时,这两个值应该是差不多相等的(可能由于插值的不同和浮点类型的精度问题会有一些差距),因此我们认为 B 不在阴影中而和往常一样进行计算。当光栅化 A 点的时候,我们发现储存的深度值明显比 A 到光源的距离要小。所以我们认为 A 在阴影中,并且在 A 点上应用一些阴影参数,以使它比以往黑一点。

这个简言之就是阴影映射算法(在第一次渲染过程中我们渲染的深度缓冲区被称为 "shadow map" )。我们将分两个阶段学习它。在第一个阶段(本节)我们将学习如何将深度信息渲染到 shadow map 中。渲染一些东西(深度,颜色等等)到由应用程序创建的纹理,被称为 'render to texture' 。我们将用十分熟悉的纹理贴图技术在屏幕上显示 shadow map 。这是一个很好的调试过程,因为保证 shadow map 的正确性对于正确实现整个阴影效果至关重要。在下一节我们将看见如何使用 shadow map 来计算顶点“是否处于阴影中”。

这一节我们使用的模型包括一个可以被用来显示 shadow map 的简单四边形。这个四边形是由两个三角形组成的,并设置纹理坐标使它们覆盖整个纹理。当四边形被渲染的时候,纹理坐标被光栅器插值,于是你就可以在整个纹理上采样并在屏幕上显示。


  
  1. (shadow_map_fbo.h:50)
  2. class ShadowMapFBO
  3. {
  4. public:
  5. ShadowMapFBO();
  6. ~ShadowMapFBO();
  7. bool Init(unsigned int WindowWidth, unsigned int WindowHeight);
  8. void BindForWriting();
  9. void BindForReading(GLenum TextureUnit);
  10. private:
  11. GLuint m_fbo;
  12. GLuint m_shadowMap;
  13. };




  1. COLOR_ATTACHMENTi —— 附着到这里的纹理将接收来自片元着色器的颜色。‘i’ 后缀意味着可以有多个纹理同时被附着为颜色附着点。在片元着色器中有一个机制可以确保同时将颜色输出到多个缓冲区中。
  2. DEPTH_ATTACHMENT —— 附着在上面的纹理将收到深度测试的结果。
  3. STENCIL_ATTACHMENT —— 附着在上面的纹理将充当模板缓冲区。模板缓冲区限制了光栅化的区域,可被用于不同的技术。
  4. DEPTH_STENCIL_ATTACHMENT —— 这仅是一个深度和模板缓冲区的结合,因为它俩经常被一起使用。

为实现阴影贴图技术,我们只需要得到经过深度测试之后获得的场景的深度值。成员属性 'm_shadowMap' 是一个纹理的句柄,这个纹理将要附着到DEPTH_ATTACHMENT 附着点上。ShadowMapFBO 也提供两个接口供主渲染函数的调用。在渲染进 shadow map 之前我们将调用 BindForWriting(),在第二次渲染过程开始的时候调用 BindForReading()。


  
  1. (shadow_map_fbo.cpp:43)
  2. glGenFramebuffers(1, &m_fbo);



  
  1. (shadow_map_fbo.cpp:46)
  2. glGenTextures(1, &m_shadowMap);
  3. glBindTexture(GL_TEXTURE_2D, m_shadowMap);
  4. glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
  5. glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  6. glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  7. glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
  8. glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);


  1. 纹理的内部格式是 GL_DEPTH_COMPONENT 。之前我们通常将纹理的内部格式设置为与颜色有关的类型如(GL_RGB),但是这里我们将其设置为 GL_DEPTH_COMPONENT 意味着纹理中的每个纹素都存放着一个单精度浮点数用于存放已经标准化后深度值。
  2. glTexImage2D 的最后一个参数是空,这意味着我们不提供任何用于初始化 buffer 的数据,因为我们想要 buffer 包含每一帧的深度值并且每一帧的深度值都可能会变化。无论我们什么时候开始一个新的帧,我们会用 glClear() 清除 buffer。这就是我们在初始化过程中需要做的。
  3. 我们告诉 OpenGL 如果纹理坐标越界,需要将其截断到[0,1]之间。当以相机为视口的投影窗口超过以光源为视口的投影窗口时会发生纹理坐标越界。为了避免奇怪的现象比如阴影在别的地方重复(由于 wraparound ),我们截断纹理坐标。


  
  1. (shadow_map_fbo.cpp:54)
  2. glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
绑定 FBO

  
  1. (shadow_map_fbo.cpp:55)
  2. glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);

深度附着点 最后一个参数 如果这里我们使用 0


  
  1. (shadow_map_fbo.cpp:58)
  2. glDrawBuffer(GL_NONE);
  3. glReadBuffer(GL_NONE);



  
  1. (shadow_map_fbo.cpp:61)
  2. GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
  3. if (Status != GL_FRAMEBUFFER_COMPLETE) {
  4. printf("FB error, status: 0x%x\n", Status);
  5. return false;
  6. }

确认其状态


  
  1. (shadow_map_fbo.cpp:72)
  2. void ShadowMapFBO::BindForWriting()
  3. {
  4. glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
  5. }

在渲染过程章我们需要将渲染目标在 shadow map 和默认的 framebuffer 之间进行切换


  
  1. (shadow_map_fbo.cpp:78)
  2. void ShadowMapFBO::BindForReading(GLenum TextureUnit)
  3. {
  4. glActiveTexture(TextureUnit);
  5. glBindTexture(GL_TEXTURE_2D, m_shadowMap);
  6. }

这个函数在第二次渲染之前被调用以绑定 shadow map 用于读取数据


  
  1. (shadow_map.vs)
  2. #version 330
  3. layout (location = 0) in vec3 Position;
  4. layout (location = 1) in vec2 TexCoord;
  5. layout (location = 2) in vec3 Normal;
  6. uniform mat4 gWVP;
  7. out vec2 TexCoordOut;
  8. void main()
  9. {
  10. gl_Position = gWVP * vec4(Position, 1.0);
  11. TexCoordOut = TexCoord;
  12. }



  
  1. (shadow_map.fs)
  2. #version 330
  3. in vec2 TexCoordOut;
  4. uniform sampler2D gShadowMap;
  5. out vec4 FragColor;
  6. void main()
  7. {
  8. float Depth = texture(gShadowMap, TexCoordOut).x;
  9. Depth = 1.0 - (1.0 - Depth) * 25.0;
  10. FragColor = vec4(Depth);
  11. }

为使效果明显



  
  1. (tutorial23.cpp:109)
  2. virtual void RenderSceneCB()
  3. {
  4. m_pGameCamera->OnRender();
  5. m_scale += 0.05f;
  6. ShadowMapPass();
  7. RenderPass();
  8. glutSwapBuffers();
  9. }
将深度信息渲染进入 shadow map 纹理中 显示这个纹理

  
  1. (tutorial23.cpp:120)
  2. virtual void ShadowMapPass()
  3. {
  4. m_shadowMapFBO.BindForWriting();
  5. glClear(GL_DEPTH_BUFFER_BIT);
  6. Pipeline p;
  7. p.Scale(0.1f, 0.1f, 0.1f);
  8. p.Rotate(0.0f, m_scale, 0.0f);
  9. p.WorldPos(0.0f, 0.0f, 5.0f);
  10. p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));
  11. p.SetPerspectiveProj(20.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);
  12. m_pShadowMapTech->SetWVP(p.GetWVPTrans());
  13. m_pMesh->Render();
  14. glBindFramebuffer(GL_FRAMEBUFFER, 0);
  15. }

先绑定 FBO 所有的深度值将被渲染进入 shadow map 聚光灯的位置和方向 然后通过绑定 FBO 为 0 来切换回默认的 framebuffer。


  
  1. (tutorial23.cpp:138)
  2. virtual void RenderPass()
  3. {
  4. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  5. m_pShadowMapTech->SetTextureUnit(0);
  6. m_shadowMapFBO.BindForReading(GL_TEXTURE0);
  7. Pipeline p;
  8. p.Scale(5.0f, 5.0f, 5.0f);
  9. p.WorldPos(0.0f, 0.0f, 10.0f);
  10. p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
  11. p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);
  12. m_pShadowMapTech->SetWVP(p.GetWVPTrans());
  13. m_pQuad->Render();
  14. }
这些缓存属于默认的 framebuffer 我们告诉着色器使用纹理单元 0,并绑定 shadow map 用来读取其中的数据

操作结果:



文章来源: panda1234lee.blog.csdn.net,作者:panda1234lee,版权归原作者所有,如需转载,请联系作者。

原文链接:panda1234lee.blog.csdn.net/article/details/51747359

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。