凡科网站建设注册,中小型企业 公司网站建设,国外设计师wordpress主题,哈尔滨最新情况教程 23阴影贴图1原文#xff1a; http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.htmlCSDN完整版专栏#xff1a; https://blog.csdn.net/cordova/article/category/9266966背景阴影和光是紧密联系的#xff0c;正如你需要光才能投射出阴影。有许多的技术可以生成…教程 23阴影贴图1原文 http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.htmlCSDN完整版专栏 https://blog.csdn.net/cordova/article/category/9266966背景阴影和光是紧密联系的正如你需要光才能投射出阴影。有许多的技术可以生成阴影在接下来的两个章节中我们将学习一种基础而简单的技术-阴影贴图。当涉及到光栅化和阴影的问题时你可能会问这个像素是否位于阴影中?或者说从光源到像素的路径是否通过其他物体如果是这个像素可能位于阴影中假定其他的物体不透明否则则像素不位于阴影中。从某种程度上讲这个问题类似于我们在之前的教程中问的问题如何确定当两个物体重叠时我们看到的是比较近的那个如果我们把相机放在光源的位置那么这两个问题就是一会儿事儿了。我们希望在深度测试中落后的像素是因为像素处于阴影中。只有在在深度测试中获胜的像素才会受到光的照射。这些像素都是直接和光源接触的其间没有任何东西会遮蔽它们。这就是在阴影贴图背后的原理。看似深度测试可以帮助我们检测一个像素是否位于阴影中但是还有一个问题相机和光源并不总是位于同一个地方。深度测试通常用于解决从相机角度看物体是否可见的问题。那么当光源处于远处的时候我们如何利用深度测试来进行阴影测试解决方案是渲染场景两次。首先从光源的角度来看此时渲染通道的结果并没有存储到颜色缓冲区中相反离光源最近的深度值被渲染到应用程序创建的深度缓冲区中而不是由GLUT自动生成的其次从摄像机的角度来看场景我们创建的深度缓冲区被绑定到片元着色器以便读取。对于每一个像素我们从这个深度缓冲区中取出相应的深度值同时我们也计算这个像素到光源的距离。有时候这两个深度值是相等的。说明这个像素与光源最近因此它的深度值才会被写进深度缓冲区此时这个像素就被认为处于光照中会和正常情况一样去计算它的颜色。如果这两个深度值不同意味着从光源看这个像素时有其他像素遮挡了它这种情况下我们在颜色计算中要增加阴影因子来模仿阴影效果。看下面这幅图 以上场景由两个对象组成——物体表面和立方体。光源是位于左上角并且指向立方体。在第一次渲染过程中我们从光源的角度呈现深度缓冲区。看图中ABC这3个点。当B被渲染时它的深度值进入深度缓冲区因为在B和光源之间没有任何东西我们默认它是那条线上离光源最近的点。然而当A和C被渲染的时候它们在深度缓冲区的同一个点上“竞争”。两个点都在同一条来自光源的直线上所以在透视投影后光栅器发现这两个点需要去往屏幕上的同一个像素。这就是深度测试最后C点“赢”了则C点的深度值被写入了深度缓存中。在第二个渲染过程中我们从摄像机的角度渲染表面和立方体。我们在着色器中除了为每个像素做一些计算我们还计算从光源到像素之间的距离并和在深度缓冲区中对应的深度值进行比较。当我们光栅化B点时这两个值应该是差不多相等的可能由于插值的不同和浮点类型的精度问题会有一些差距因此我们认为B不在阴影中而和往常一样进行计算。当光栅化A点的时候我们发现储存的深度值明显比A到光源的距离要小。所以我们认为A在阴影中并且在A点上应用一些阴影参数使它比以往暗一些。简言之这就是阴影映射算法我们在第一次渲染通道中渲染的深度缓冲称为“阴影贴图”我们将分两个阶段学习它。在第一个阶段本节我们将学习如何将深度信息渲染到阴影图中渲染一个由应用程序创建的纹理被称为 纹理渲染 我们将使用一个简单的纹理映射技术在屏幕上显示阴影贴图这是一个很好的调试过程为了得到完整的阴影效果正确的绘制阴影贴图是至关重要的。在下一节我们将看见如何使用阴影图来计算顶点“是否处于阴影中”。这一节我们使用的模型是一个简单的可以用来显示阴影贴图的四边形网格。这个四边形是由两个三角形组成的并设置纹理坐标使它们覆盖整个纹理。当四边形被渲染的时候纹理坐标被光栅器插值于是就可以采样整个纹理并将其显示在屏幕上。源代码详解(shadow_map_fbo.h:50)class ShadowMapFBO
{public:ShadowMapFBO();~ShadowMapFBO();bool Init(unsigned int WindowWidth, unsigned int WindowHeight);void BindForWriting();void BindForReading(GLenum TextureUnit);private:GLuint m_fbo;GLuint m_shadowMap;
};
在OpenGL中3d管线输出的结果称为帧缓冲对象‘简称FBO。FBO可以挂载颜色缓冲在屏幕上显示、深度缓冲区和一些有其他用处的缓冲区。当glutInitDisplayMode()被调用的时候它使用一些特定的参数来创建默认的帧缓存这个帧缓存被窗口系统所管理不会被OpenGL删除。除了默认的帧缓存应用程序可以创建自己的FBOs。在应用程序的控制下这些对象可以被操作以用于不同的技术当中。ShadowMapFBO类为FBO提供一个容易操作的接口会被FBO用来实现阴影贴图技术。ShadowMapFBO类内部有两个OpenGL句柄其中m_fbo句柄代表真正的FBOFBO封装了帧缓存所有的状态一旦这个对象被创建并设置合适的参数我们就可以简单的通过绑定不同的对象来改变帧缓存。注意只有默认的帧缓存才可以在屏幕上显示。应用程序创建的帧缓存只能用于”离屏渲染“这个可以说是一个中间的渲染过程比如我们的阴影贴图缓冲区稍后可以用于屏幕上的“真实”渲染通道。就其本身而言帧缓存只是一个占位符为了使它变得可用我们需要把纹理依附于一个或者更多的可用的挂载点纹理含有帧缓存实际的内存空间。OpenGL定义了下面的一些附着点:COLOR_ATTACHMENTi:附着到这里的纹理将接收来自片元着色器的颜色。‘i’ 后缀意味着可以有多个纹理同时被附着为颜色附着点。在片元着色器中有一个机制可以确保同时渲染多个颜色到缓冲区。DEPTH_ATTACHMENT:附着在上面的纹理将收到深度测试的结果。STENCIL_ATTACHMENT:附着在上面的纹理将充当模板缓冲区。模板缓冲区限制了光栅化的区域可被用于不同的技术。DEPTH_STENCIL_ATTACHMENT:这仅是一个深度和模板缓冲区的结合因为它俩经常被一起使用。对于阴影映射技术我们只需要一个深度缓冲。成员属性“m_shadowmap“是附加到DEPTH_ATTACHMENT附着点的纹理句柄。ShadowMapFBO也提供了一些方法主要用在渲染功能上。在开始第二次渲染的时候我们要在渲染到阴影图和BindForReading()之前调用BindForWriting()。(shadow_map_fbo.cpp:43)glGenFramebuffers(1, m_fbo);这里我们创建FBO。和纹理与缓冲区这些对象的创建方式一样我们指定一个GLuints数组的地址和它的大小这个数组被句柄填充。(shadow_map_fbo.cpp:46)glGenTextures(1, m_shadowMap);
glBindTexture(GL_TEXTURE_2D, m_shadowMap);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
接下来我们创建纹理来作为阴影图。在一般情况下这是一个标准的有特定配置的2D纹理使其用于达到以下目的纹理的内部格式是 GL_DEPTH_COMPONENT 。和之前不同之前我们通常将纹理的内部格式设置为与颜色有关的类型如GL_RGB这里我们将其设置为 GL_DEPTH_COMPONENT意味着纹理中的每个纹素都存放着一个单精度浮点数用于存放已经标准化后的深度值。glTexImage2D的最后一个参数是空意味着我们不提供任何用于初始化buffer的数据因为我们想让buffer包含每一帧的深度值并且每一帧的深度值都可能会变化。无论我们何时开始一个新的帧我们都要用glClear()清除buffer。这些是我们在初始化过程中要做的。我们告诉OpenGL如果纹理坐标越界需要将其截断到[01]之间。当以相机为视口的投影窗口超过以光源为视口的投影窗口时会发生纹理坐标越界。为了避免不好的现象比如由于wraparound的原因阴影在别的地方重复出现我们要截断纹理坐标。 (shadow_map_fbo.cpp:54)glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);我们已经生成FBO纹理对象并为阴影贴图配置了纹理对象现在我们需要把纹理对象附到FBO。我们要做的第一件事就是绑定FBO之后所有对FBO的操作都会对它产生影响。这个函数的参数是FBO句柄和所需的target。target可以是GL_FRAMEBUFFER,GL_DRAW_FRAMEBUFFER或者GL_READ_FRAMEBUFFER。GL_READ_FRAMEBUFFE在我们想调用glReadPixels本教程中不会使用从FBO中读取内容时会用到当我们想要把场景渲染进入FBO时需要使用GL_DRAW_FRAMEBUFFE当我们使用GL_FRAMEBUFFER时FBO的读写状态都会被更新建议这样初始化FBO当我们真正开始渲染的时候我们会使用GL_DRAW_FRAMEBUFFER。(shadow_map_fbo.cpp:55) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);这里我们把shadow map纹理附着到FBO的深度附着点上。这个函数最后一个参数指明要用的Mipmap层级。Mipmap层是纹理贴图的一个特性以不同分辨率展现一个纹理。0代表最大的分辨率随着层级的增加纹理的分辨率会越来越小。将Mipmap纹理和三线性滤波结合起来能产生更好的结果。这里我们只有一个mipmap层所以我们使用0。我们让shadow map句柄作为第四个参数。如果这里我们使用0那么当前的纹理(在上面的例子是深度将从指定的附着点上脱落。(shadow_map_fbo.cpp:58)
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
因为我们没打算渲染到color buffer只输出深度我们通过上面的函数来禁止向颜色缓存中写入。默认情况下颜色缓存会被绑定在GL_COLOR_ATTACHMENT0上但是我们的FBO中甚至不会包含一个纹理缓冲区所以最好明确的告诉OpenGL我们的目的。这个函数可用的参数是GL_NONE和GL_COLOR_ATTACHMENT0到 GL_COLOR_ATTACHMENTm‘m’是GL_MAX_COLOR_ATTACHMENTS–1。这些参数只对FBOs有效。如果用了默认的framebuffer那么有效的参数是GL_NONE, GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFT和GL_BACK_RIGHT这使你可以直接将场景渲染到front buffer或者back buffer每一个都有左left和right buffer。我们也将从缓存中的读取操作设置为GL_NONE注意我们不打算调用glReadPixel APIs中的任何一个函数。这主要是为了避免因GPU只支持 opengl3.x而不支持4.x而出现问题。(shadow_map_fbo.cpp:61)GLenum Status glCheckFramebufferStatus(GL_FRAMEBUFFER);if (Status ! GL_FRAMEBUFFER_COMPLETE) {printf(FB error, status: 0x%xn, Status);return false;
}
当我们完成FBO的配置后一定要确认其状态是否为OpenGL定义的“complete”确保没有错误出现并且framebuffer现在是可用的了。上面就是检验这个的代码。 (shadow_map_fbo.cpp:72)void ShadowMapFBO::BindForWriting()
{glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
}
在渲染过程中我们需要将渲染目标在shadow map和默认的framebuffer之间进行切换。在第二个渲染过程中我们要绑定shadow map作为输入。这个函数和下一个函数将这个工作封装起来便于调用。上面的函数仅绑定FBO用于写入数据在第一次渲染之前我们将调用它。(shadow_map_fbo.cpp:78)void ShadowMapFBO::BindForReading(GLenum TextureUnit)
{glActiveTexture(TextureUnit);glBindTexture(GL_TEXTURE_2D, m_shadowMap);
}
这个函数在第二次渲染之前被调用以绑定shadow map用于读取数据。注意我们是绑定纹理对象而不是FBO本身。这个函数的参数是纹理单元并把shadow map绑定到这个纹理单元上。这个纹理单元的索引一定要和着色器同步因为着色器有一个sampler2D一致变量用来访问这个纹理。注意glActiveTexture的参数是纹理索引的枚举值比如GL_TEXTURE0,GL_TEXTURE1等着色器中的一致变量只需要索引值本身如01等这可能会导致很多bug出现。(shadow_map.vs)#version 330layout (location 0) in vec3 Position;
layout (location 1) in vec2 TexCoord;
layout (location 2) in vec3 Normal;uniform mat4 gWVP;out vec2 TexCoordOut;void main()
{gl_Position gWVP * vec4(Position, 1.0);TexCoordOut TexCoord;
}
我们将在两次的渲染中都使用同一着色器程序。顶点着色器在两次渲染过程中都用得到而片元着色器将只在第二次渲染过程中被使用。因为我们在第一次渲染过程中禁止把数据写入颜色缓存所以就没用到片元着色器。上面的顶点着色器是十分简单的它仅仅是通过WVP矩阵将位置坐标变换到裁剪坐标系中并将纹理坐标传递到片元着色器中。在第一次的渲染过程中纹理坐标是多余的因为没有片元着色器。然而这没有实际的影响。可以看出从着色器角度来看无论这是一个渲染深度的过程还是一个真正的渲染过程都没有什么不同而真正不同的地方是应用程序在第一次渲染过程传递的是以光源为视口的WVP矩阵而在第二次渲染过程传递的是以相机为视口的WVP矩阵。在第一次的渲染过程Z buffer将用最靠近光源位置的Z值所填充在第二次渲染过程中Z buffer将被最靠近相机位置的Z值所填充。在第二次渲染过程中我们需要使用片元着色器中的纹理坐标因为我们将从shadow map此时它是着色器的输入中进行采样。(shadow_map.fs)#version 330in vec2 TexCoordOut;
uniform sampler2D gShadowMap;out vec4 FragColor;void main()
{float Depth texture(gShadowMap, TexCoordOut).x;Depth 1.0 - (1.0 - Depth) * 25.0;FragColor vec4(Depth);
}
这是在渲染过程中用来显示shadow map的片元着色器。2D纹理坐标用来从shadow map中进行采样。Shadow map纹理是以GL_DEPTH_COMPONENT类型为内部格式而创建的意味着纹理中每一个纹素都是一个单精度的浮点型数据而不是一种颜色。这就是为什么在采样的过程中要使用.x。当我们显示深度缓存中的内容时我们可能遇到的一个情况是渲染的结果不够清楚。所以在我们从shadow map中采样获得深度值后为使效果明显我们放大当前点的距离到远边缘(此处Z为1)然后再用1减去这个放大后值。我们将这个值作为片元的每个颜色通道的值意味着我们将得到一些灰度的变化远裁剪面处是白色近裁剪面处是黑色。现在我们如何结合上面的这些代码片段来创建应用程序。(tutorial23.cpp:106)
virtual void RenderSceneCB()
{m_pGameCamera-OnRender();m_scale 0.05f;ShadowMapPass();RenderPass();glutSwapBuffers();
}
主渲染程序随着大部分的功能移到其他函数中变得更加简单了。我们先处理全局的东西比如更新相机的位置和用来旋转对象的类成员。然后我们调用一个ShadowMapPass()函数将深度信息渲染到shadow map纹理中接着用RenderPass()函数来显示这个纹理。最后调用glutSwapBuffer()来将最终结果显示到屏幕上。 (tutorial23.cpp:117)virtual void ShadowMapPass()
{m_shadowMapFBO.BindForWriting();glClear(GL_DEPTH_BUFFER_BIT);Pipeline p;p.Scale(0.1f, 0.1f, 0.1f);p.Rotate(0.0f, m_scale, 0.0f);p.WorldPos(0.0f, 0.0f, 5.0f);p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));p.SetPerspectiveProj(20.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);m_pShadowMapTech-SetWVP(p.GetWVPTrans());m_pMesh-Render();glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
在渲染Shadow map之前我们先绑定FBO。从现在起所有的深度值将被渲染到shadow map中同时舍弃颜色的写入过程。我们只在渲染开始之前清除深度缓冲区之后我们为了渲染mesh例子为一个坦克初始化了一个pipeline类对象。这里值得注意的一点是相机相关设置是基于聚光灯的位置和方向的。我们先渲染mesh然后通过绑定FBO为0来切换回默认的framebuffer。(tutorial23.cpp:135)virtual void RenderPass()
{glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);m_pShadowMapTech-SetTextureUnit(0);m_shadowMapFBO.BindForReading(GL_TEXTURE0);Pipeline p;p.Scale(5.0f, 5.0f, 5.0f);p.WorldPos(0.0f, 0.0f, 10.0f);p.SetCamera(m_pGameCamera-GetPos(), m_pGameCamera-GetTarget(), m_pGameCamera-GetUp());p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);m_pShadowMapTech-SetWVP(p.GetWVPTrans());m_pQuad-Render();
}
在第二个渲染过程开始前我们先清除颜色和深度缓存这些缓冲区属于默认的帧缓存。我们告诉着色器使用纹理单元0并绑定阴影贴图用来读取其中的数据。从这里开始处理就都和以前一样了。我们放大四边形把它直接放在相机的前面并渲染它。在光栅化期间进行采样阴影贴图并将其显示到模型上。注意在这个教程的代码中当网格文件没有指定一个纹理时我们不再自动加载一个白色的纹理因为现在可以绑定阴影贴图来代替。如果网格不包含纹理我们就什么都不绑定而是调用代码让其绑定自己的纹理。