opengl 深度详解_一步步学OpenGL(23) -《阴影贴图1》
教程 23
陰影貼圖1
原文: http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html
CSDN完整版專欄: https://blog.csdn.net/cordova/article/category/9266966
背景
陰影和光是緊密聯系的,正如你需要光才能投射出陰影。有許多的技術可以生成陰影,在接下來的兩個章節中我們將學習一種基礎而簡單的技術-陰影貼圖。
當涉及到光柵化和陰影的問題時,你可能會問這個像素是否位于陰影中?或者說,從光源到像素的路徑是否通過其他物體?如果是,這個像素可能位于陰影中(假定其他的物體不透明),否則,則像素不位于陰影中。從某種程度上講,這個問題類似于我們在之前的教程中問的問題:如何確定當兩個物體重疊時,我們看到的是比較近的那個?如果我們把相機放在光源的位置,那么這兩個問題就是一會兒事兒了。我們希望在深度測試中落后的像素是因為像素處于陰影中。只有在在深度測試中獲勝的像素才會受到光的照射。這些像素都是直接和光源接觸的,其間沒有任何東西會遮蔽它們。這就是在陰影貼圖背后的原理。
看似深度測試可以幫助我們檢測一個像素是否位于陰影中,但是還有一個問題:相機和光源并不總是位于同一個地方。深度測試通常用于解決從相機角度看物體是否可見的問題。那么當光源處于遠處的時候,我們如何利用深度測試來進行陰影測試?解決方案是渲染場景兩次。首先從光源的角度來看,此時渲染通道的結果并沒有存儲到顏色緩沖區中,相反,離光源最近的深度值被渲染到應用程序創建的深度緩沖區中(而不是由GLUT自動生成的);其次,從攝像機的角度來看場景,我們創建的深度緩沖區被綁定到片元著色器以便讀取。對于每一個像素,我們從這個深度緩沖區中取出相應的深度值,同時我們也計算這個像素到光源的距離。有時候這兩個深度值是相等的。說明這個像素與光源最近,因此它的深度值才會被寫進深度緩沖區,此時,這個像素就被認為處于光照中會和正常情況一樣去計算它的顏色。如果這兩個深度值不同,意味著從光源看這個像素時有其他像素遮擋了它,這種情況下我們在顏色計算中要增加陰影因子來模仿陰影效果。看下面這幅圖:
以上場景由兩個對象組成——物體表面和立方體。光源是位于左上角并且指向立方體。在第一次渲染過程中,我們從光源的角度呈現深度緩沖區。看圖中A,B,C這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'句柄代表真正的FBO,FBO封裝了幀緩存所有的狀態,一旦這個對象被創建并設置合適的參數,我們就可以簡單的通過綁定不同的對象來改變幀緩存。注意只有默認的幀緩存才可以在屏幕上顯示。應用程序創建的幀緩存只能用于”離屏渲染“,這個可以說是一個中間的渲染過程(比如我們的陰影貼圖緩沖區),稍后可以用于屏幕上的“真實”渲染通道。
就其本身而言,幀緩存只是一個占位符,為了使它變得可用,我們需要把紋理依附于一個或者更多的可用的掛載點,紋理含有幀緩存實際的內存空間。OpenGL定義了下面的一些附著點:
- COLOR_ATTACHMENTi:附著到這里的紋理將接收來自片元著色器的顏色?!甶’ 后綴意味著可以有多個紋理同時被附著為顏色附著點。在片元著色器中有一個機制可以確保同時渲染多個顏色到緩沖區。
- 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紋理,使其用于達到以下目的:
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等),著色器中的一致變量只需要索引值本身(如0,1等),這可能會導致很多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矩陣將位置坐標變換到裁剪坐標系中,并將紋理坐標傳遞到片元著色器中。在第一次的渲染過程中,紋理坐標是多余的(因為沒有片元著色器)。然而,這沒有實際的影響??梢钥闯?#xff0c;從著色器角度來看,無論這是一個渲染深度的過程還是一個真正的渲染過程都沒有什么不同,而真正不同的地方是應用程序在第一次渲染過程傳遞的是以光源為視口的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,并綁定陰影貼圖用來讀取其中的數據。從這里開始處理就都和以前一樣了。我們放大四邊形,把它直接放在相機的前面并渲染它。在光柵化期間進行采樣陰影貼圖并將其顯示到模型上。
注意:在這個教程的代碼中,當網格文件沒有指定一個紋理時,我們不再自動加載一個白色的紋理,因為現在可以綁定陰影貼圖來代替。如果網格不包含紋理我們就什么都不綁定,而是調用代碼讓其綁定自己的紋理。
總結
以上是生活随笔為你收集整理的opengl 深度详解_一步步学OpenGL(23) -《阴影贴图1》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java phantomjs 截图_ph
- 下一篇: python导入同一文件夹下的类_pyt