opengl 深度详解_一步步学OpenGL(23) -《阴影贴图1》
教程 23
陰影貼圖1
原文: http://ogldev.atspace.co.uk/www/tutorial23/tutorial23.html
CSDN完整版專欄: https://blog.csdn.net/cordova/article/category/9266966
背景
陰影和光是緊密聯(lián)系的,正如你需要光才能投射出陰影。有許多的技術(shù)可以生成陰影,在接下來的兩個(gè)章節(jié)中我們將學(xué)習(xí)一種基礎(chǔ)而簡單的技術(shù)-陰影貼圖。
當(dāng)涉及到光柵化和陰影的問題時(shí),你可能會(huì)問這個(gè)像素是否位于陰影中?或者說,從光源到像素的路徑是否通過其他物體?如果是,這個(gè)像素可能位于陰影中(假定其他的物體不透明),否則,則像素不位于陰影中。從某種程度上講,這個(gè)問題類似于我們在之前的教程中問的問題:如何確定當(dāng)兩個(gè)物體重疊時(shí),我們看到的是比較近的那個(gè)?如果我們把相機(jī)放在光源的位置,那么這兩個(gè)問題就是一會(huì)兒事兒了。我們希望在深度測試中落后的像素是因?yàn)橄袼靥幱陉幱爸小V挥性谠谏疃葴y試中獲勝的像素才會(huì)受到光的照射。這些像素都是直接和光源接觸的,其間沒有任何東西會(huì)遮蔽它們。這就是在陰影貼圖背后的原理。
看似深度測試可以幫助我們檢測一個(gè)像素是否位于陰影中,但是還有一個(gè)問題:相機(jī)和光源并不總是位于同一個(gè)地方。深度測試通常用于解決從相機(jī)角度看物體是否可見的問題。那么當(dāng)光源處于遠(yuǎn)處的時(shí)候,我們?nèi)绾卫蒙疃葴y試來進(jìn)行陰影測試?解決方案是渲染場景兩次。首先從光源的角度來看,此時(shí)渲染通道的結(jié)果并沒有存儲(chǔ)到顏色緩沖區(qū)中,相反,離光源最近的深度值被渲染到應(yīng)用程序創(chuàng)建的深度緩沖區(qū)中(而不是由GLUT自動(dòng)生成的);其次,從攝像機(jī)的角度來看場景,我們創(chuàng)建的深度緩沖區(qū)被綁定到片元著色器以便讀取。對于每一個(gè)像素,我們從這個(gè)深度緩沖區(qū)中取出相應(yīng)的深度值,同時(shí)我們也計(jì)算這個(gè)像素到光源的距離。有時(shí)候這兩個(gè)深度值是相等的。說明這個(gè)像素與光源最近,因此它的深度值才會(huì)被寫進(jìn)深度緩沖區(qū),此時(shí),這個(gè)像素就被認(rèn)為處于光照中會(huì)和正常情況一樣去計(jì)算它的顏色。如果這兩個(gè)深度值不同,意味著從光源看這個(gè)像素時(shí)有其他像素遮擋了它,這種情況下我們在顏色計(jì)算中要增加陰影因子來模仿陰影效果。看下面這幅圖:
以上場景由兩個(gè)對象組成——物體表面和立方體。光源是位于左上角并且指向立方體。在第一次渲染過程中,我們從光源的角度呈現(xiàn)深度緩沖區(qū)。看圖中A,B,C這3個(gè)點(diǎn)。當(dāng)B被渲染時(shí),它的深度值進(jìn)入深度緩沖區(qū),因?yàn)樵贐和光源之間沒有任何東西,我們默認(rèn)它是那條線上離光源最近的點(diǎn)。然而當(dāng)A和C被渲染的時(shí)候,它們在深度緩沖區(qū)的同一個(gè)點(diǎn)上“競爭”。兩個(gè)點(diǎn)都在同一條來自光源的直線上,所以在透視投影后,光柵器發(fā)現(xiàn)這兩個(gè)點(diǎn)需要去往屏幕上的同一個(gè)像素。這就是深度測試,最后C點(diǎn)“贏”了,則C點(diǎn)的深度值被寫入了深度緩存中。
在第二個(gè)渲染過程中,我們從攝像機(jī)的角度渲染表面和立方體。我們在著色器中除了為每個(gè)像素做一些計(jì)算,我們還計(jì)算從光源到像素之間的距離,并和在深度緩沖區(qū)中對應(yīng)的深度值進(jìn)行比較。當(dāng)我們光柵化B點(diǎn)時(shí),這兩個(gè)值應(yīng)該是差不多相等的(可能由于插值的不同和浮點(diǎn)類型的精度問題會(huì)有一些差距),因此我們認(rèn)為B不在陰影中而和往常一樣進(jìn)行計(jì)算。當(dāng)光柵化A點(diǎn)的時(shí)候,我們發(fā)現(xiàn)儲(chǔ)存的深度值明顯比A到光源的距離要小。所以我們認(rèn)為A在陰影中,并且在A點(diǎn)上應(yīng)用一些陰影參數(shù),使它比以往暗一些。
簡言之,這就是陰影映射算法(我們在第一次渲染通道中渲染的深度緩沖稱為“陰影貼圖”),我們將分兩個(gè)階段學(xué)習(xí)它。在第一個(gè)階段(本節(jié))我們將學(xué)習(xí)如何將深度信息渲染到陰影圖中,渲染一個(gè)由應(yīng)用程序創(chuàng)建的紋理,被稱為 '紋理渲染 ;我們將使用一個(gè)簡單的紋理映射技術(shù)在屏幕上顯示陰影貼圖,這是一個(gè)很好的調(diào)試過程,為了得到完整的陰影效果,正確的繪制陰影貼圖是至關(guān)重要的。在下一節(jié)我們將看見如何使用陰影圖來計(jì)算頂點(diǎn)“是否處于陰影中”。
這一節(jié)我們使用的模型是一個(gè)簡單的可以用來顯示陰影貼圖的四邊形網(wǎng)格。這個(gè)四邊形是由兩個(gè)三角形組成的,并設(shè)置紋理坐標(biāo)使它們覆蓋整個(gè)紋理。當(dāng)四邊形被渲染的時(shí)候,紋理坐標(biāo)被光柵器插值,于是就可以采樣整個(gè)紋理并將其顯示在屏幕上。
源代碼詳解
(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管線輸出的結(jié)果稱為'幀緩沖對象‘(簡稱FBO)。FBO可以掛載顏色緩沖(在屏幕上顯示)、深度緩沖區(qū)和一些有其他用處的緩沖區(qū)。當(dāng)glutInitDisplayMode()被調(diào)用的時(shí)候,它使用一些特定的參數(shù)來創(chuàng)建默認(rèn)的幀緩存,這個(gè)幀緩存被窗口系統(tǒng)所管理,不會(huì)被OpenGL刪除。除了默認(rèn)的幀緩存,應(yīng)用程序可以創(chuàng)建自己的FBOs。在應(yīng)用程序的控制下,這些對象可以被操作以用于不同的技術(shù)當(dāng)中。ShadowMapFBO類為FBO提供一個(gè)容易操作的接口,會(huì)被FBO用來實(shí)現(xiàn)陰影貼圖技術(shù)。ShadowMapFBO類內(nèi)部有兩個(gè)OpenGL句柄,其中'm_fbo'句柄代表真正的FBO,FBO封裝了幀緩存所有的狀態(tài),一旦這個(gè)對象被創(chuàng)建并設(shè)置合適的參數(shù),我們就可以簡單的通過綁定不同的對象來改變幀緩存。注意只有默認(rèn)的幀緩存才可以在屏幕上顯示。應(yīng)用程序創(chuàng)建的幀緩存只能用于”離屏渲染“,這個(gè)可以說是一個(gè)中間的渲染過程(比如我們的陰影貼圖緩沖區(qū)),稍后可以用于屏幕上的“真實(shí)”渲染通道。
就其本身而言,幀緩存只是一個(gè)占位符,為了使它變得可用,我們需要把紋理依附于一個(gè)或者更多的可用的掛載點(diǎn),紋理含有幀緩存實(shí)際的內(nèi)存空間。OpenGL定義了下面的一些附著點(diǎn):
- COLOR_ATTACHMENTi:附著到這里的紋理將接收來自片元著色器的顏色。‘i’ 后綴意味著可以有多個(gè)紋理同時(shí)被附著為顏色附著點(diǎn)。在片元著色器中有一個(gè)機(jī)制可以確保同時(shí)渲染多個(gè)顏色到緩沖區(qū)。
- DEPTH_ATTACHMENT:附著在上面的紋理將收到深度測試的結(jié)果。
- STENCIL_ATTACHMENT:附著在上面的紋理將充當(dāng)模板緩沖區(qū)。模板緩沖區(qū)限制了光柵化的區(qū)域,可被用于不同的技術(shù)。
- DEPTH_STENCIL_ATTACHMENT:這僅是一個(gè)深度和模板緩沖區(qū)的結(jié)合,因?yàn)樗鼈z經(jīng)常被一起使用。
對于陰影映射技術(shù),我們只需要一個(gè)深度緩沖。成員屬性“m_shadowmap“是附加到DEPTH_ATTACHMENT附著點(diǎn)的紋理句柄。ShadowMapFBO也提供了一些方法,主要用在渲染功能上。在開始第二次渲染的時(shí)候,我們要在渲染到陰影圖和BindForReading()之前調(diào)用BindForWriting()。
(shadow_map_fbo.cpp:43)
glGenFramebuffers(1, &m_fbo);
這里我們創(chuàng)建FBO。和紋理與緩沖區(qū)這些對象的創(chuàng)建方式一樣,我們指定一個(gè)GLuints數(shù)組的地址和它的大小,這個(gè)數(shù)組被句柄填充。
(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);接下來我們創(chuàng)建紋理來作為陰影圖。在一般情況下,這是一個(gè)標(biāo)準(zhǔn)的有特定配置的2D紋理,使其用于達(dá)到以下目的:
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
我們已經(jīng)生成FBO紋理對象,并為陰影貼圖配置了紋理對象,現(xiàn)在我們需要把紋理對象附到FBO。我們要做的第一件事就是綁定FBO,之后所有對FBO的操作都會(huì)對它產(chǎn)生影響。這個(gè)函數(shù)的參數(shù)是FBO句柄和所需的target。target可以是GL_FRAMEBUFFER,GL_DRAW_FRAMEBUFFER或者GL_READ_FRAMEBUFFER。GL_READ_FRAMEBUFFE在我們想調(diào)用glReadPixels(本教程中不會(huì)使用)從FBO中讀取內(nèi)容時(shí)會(huì)用到;當(dāng)我們想要把場景渲染進(jìn)入FBO時(shí)需要使用GL_DRAW_FRAMEBUFFE;當(dāng)我們使用GL_FRAMEBUFFER時(shí),FBO的讀寫狀態(tài)都會(huì)被更新,建議這樣初始化FBO;當(dāng)我們真正開始渲染的時(shí)候我們會(huì)使用GL_DRAW_FRAMEBUFFER。
(shadow_map_fbo.cpp:55) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_shadowMap, 0);
這里我們把shadow map紋理附著到FBO的深度附著點(diǎn)上。這個(gè)函數(shù)最后一個(gè)參數(shù)指明要用的Mipmap層級。Mipmap層是紋理貼圖的一個(gè)特性,以不同分辨率展現(xiàn)一個(gè)紋理。0代表最大的分辨率,隨著層級的增加,紋理的分辨率會(huì)越來越小。將Mipmap紋理和三線性濾波結(jié)合起來能產(chǎn)生更好的結(jié)果。這里我們只有一個(gè)mipmap層,所以我們使用0。我們讓shadow map句柄作為第四個(gè)參數(shù)。如果這里我們使用0,那么當(dāng)前的紋理(在上面的例子是深度)將從指定的附著點(diǎn)上脫落。
(shadow_map_fbo.cpp:58) glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE);因?yàn)槲覀儧]打算渲染到color buffer(只輸出深度),我們通過上面的函數(shù)來禁止向顏色緩存中寫入。默認(rèn)情況下,顏色緩存會(huì)被綁定在GL_COLOR_ATTACHMENT0上,但是我們的FBO中甚至不會(huì)包含一個(gè)紋理緩沖區(qū),所以,最好明確的告訴OpenGL我們的目的。這個(gè)函數(shù)可用的參數(shù)是GL_NONE和GL_COLOR_ATTACHMENT0到 GL_COLOR_ATTACHMENTm,‘m’是(GL_MAX_COLOR_ATTACHMENTS–1)。這些參數(shù)只對FBOs有效。如果用了默認(rèn)的framebuffer,那么有效的參數(shù)是GL_NONE, GL_FRONT_LEFT,GL_FRONT_RIGHT,GL_BACK_LEFT和GL_BACK_RIGHT,這使你可以直接將場景渲染到front buffer或者back buffer(每一個(gè)都有左left和right buffer)。我們也將從緩存中的讀取操作設(shè)置為GL_NONE(注意,我們不打算調(diào)用glReadPixel APIs中的任何一個(gè)函數(shù))。這主要是為了避免因GPU只支持 opengl3.x而不支持4.x而出現(xiàn)問題。
(shadow_map_fbo.cpp:61)
GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);if (Status != GL_FRAMEBUFFER_COMPLETE) {printf("FB error, status: 0x%xn", Status);return false; }當(dāng)我們完成FBO的配置后,一定要確認(rèn)其狀態(tài)是否為OpenGL定義的“complete”,確保沒有錯(cuò)誤出現(xiàn)并且framebuffer現(xiàn)在是可用的了。上面就是檢驗(yàn)這個(gè)的代碼。 (shadow_map_fbo.cpp:72)
void ShadowMapFBO::BindForWriting() {glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo); }在渲染過程中我們需要將渲染目標(biāo)在shadow map和默認(rèn)的framebuffer之間進(jìn)行切換。在第二個(gè)渲染過程中,我們要綁定shadow map作為輸入。這個(gè)函數(shù)和下一個(gè)函數(shù)將這個(gè)工作封裝起來便于調(diào)用。上面的函數(shù)僅綁定FBO用于寫入數(shù)據(jù),在第一次渲染之前我們將調(diào)用它。
(shadow_map_fbo.cpp:78)
void ShadowMapFBO::BindForReading(GLenum TextureUnit) {glActiveTexture(TextureUnit);glBindTexture(GL_TEXTURE_2D, m_shadowMap); }這個(gè)函數(shù)在第二次渲染之前被調(diào)用以綁定shadow map用于讀取數(shù)據(jù)。注意我們是綁定紋理對象而不是FBO本身。這個(gè)函數(shù)的參數(shù)是紋理單元,并把shadow map綁定到這個(gè)紋理單元上。這個(gè)紋理單元的索引一定要和著色器同步(因?yàn)橹饔幸粋€(gè)sampler2D一致變量用來訪問這個(gè)紋理)。注意glActiveTexture的參數(shù)是紋理索引的枚舉值(比如GL_TEXTURE0,GL_TEXTURE1等),著色器中的一致變量只需要索引值本身(如0,1等),這可能會(huì)導(dǎo)致很多bug出現(xiàn)。
(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; }我們將在兩次的渲染中都使用同一著色器程序。頂點(diǎn)著色器在兩次渲染過程中都用得到,而片元著色器將只在第二次渲染過程中被使用。因?yàn)槲覀冊诘谝淮武秩具^程中禁止把數(shù)據(jù)寫入顏色緩存,所以就沒用到片元著色器。上面的頂點(diǎn)著色器是十分簡單的,它僅僅是通過WVP矩陣將位置坐標(biāo)變換到裁剪坐標(biāo)系中,并將紋理坐標(biāo)傳遞到片元著色器中。在第一次的渲染過程中,紋理坐標(biāo)是多余的(因?yàn)闆]有片元著色器)。然而,這沒有實(shí)際的影響。可以看出,從著色器角度來看,無論這是一個(gè)渲染深度的過程還是一個(gè)真正的渲染過程都沒有什么不同,而真正不同的地方是應(yīng)用程序在第一次渲染過程傳遞的是以光源為視口的WVP矩陣,而在第二次渲染過程傳遞的是以相機(jī)為視口的WVP矩陣。在第一次的渲染過程Z buffer將用最靠近光源位置的Z值所填充,在第二次渲染過程中,Z buffer將被最靠近相機(jī)位置的Z值所填充。在第二次渲染過程中我們需要使用片元著色器中的紋理坐標(biāo),因?yàn)槲覀儗膕hadow map(此時(shí)它是著色器的輸入)中進(jìn)行采樣。
(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紋理坐標(biāo)用來從shadow map中進(jìn)行采樣。Shadow map紋理是以GL_DEPTH_COMPONENT類型為內(nèi)部格式而創(chuàng)建的,意味著紋理中每一個(gè)紋素都是一個(gè)單精度的浮點(diǎn)型數(shù)據(jù)而不是一種顏色。這就是為什么在采樣的過程中要使用'.x'。當(dāng)我們顯示深度緩存中的內(nèi)容時(shí),我們可能遇到的一個(gè)情況是渲染的結(jié)果不夠清楚。所以,在我們從shadow map中采樣獲得深度值后,為使效果明顯,我們放大當(dāng)前點(diǎn)的距離到遠(yuǎn)邊緣(此處Z為1),然后再用1減去這個(gè)放大后值。我們將這個(gè)值作為片元的每個(gè)顏色通道的值,意味著我們將得到一些灰度的變化(遠(yuǎn)裁剪面處是白色,近裁剪面處是黑色)。
現(xiàn)在我們?nèi)绾谓Y(jié)合上面的這些代碼片段來創(chuàng)建應(yīng)用程序。
(tutorial23.cpp:106) virtual void RenderSceneCB() {m_pGameCamera->OnRender();m_scale += 0.05f;ShadowMapPass();RenderPass();glutSwapBuffers(); }主渲染程序隨著大部分的功能移到其他函數(shù)中變得更加簡單了。我們先處理全局的東西,比如更新相機(jī)的位置和用來旋轉(zhuǎn)對象的類成員。然后我們調(diào)用一個(gè)ShadowMapPass()函數(shù)將深度信息渲染到shadow map紋理中,接著用RenderPass()函數(shù)來顯示這個(gè)紋理。最后調(diào)用glutSwapBuffer()來將最終結(jié)果顯示到屏幕上。 (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。從現(xiàn)在起,所有的深度值將被渲染到shadow map中,同時(shí)舍棄顏色的寫入過程。我們只在渲染開始之前清除深度緩沖區(qū),之后我們?yōu)榱虽秩緈esh(例子為一個(gè)坦克)初始化了一個(gè)pipeline類對象。這里值得注意的一點(diǎn)是相機(jī)相關(guān)設(shè)置是基于聚光燈的位置和方向的。我們先渲染mesh,然后通過綁定FBO為0來切換回默認(rèn)的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(); }在第二個(gè)渲染過程開始前,我們先清除顏色和深度緩存,這些緩沖區(qū)屬于默認(rèn)的幀緩存。我們告訴著色器使用紋理單元0,并綁定陰影貼圖用來讀取其中的數(shù)據(jù)。從這里開始處理就都和以前一樣了。我們放大四邊形,把它直接放在相機(jī)的前面并渲染它。在光柵化期間進(jìn)行采樣陰影貼圖并將其顯示到模型上。
注意:在這個(gè)教程的代碼中,當(dāng)網(wǎng)格文件沒有指定一個(gè)紋理時(shí),我們不再自動(dòng)加載一個(gè)白色的紋理,因?yàn)楝F(xiàn)在可以綁定陰影貼圖來代替。如果網(wǎng)格不包含紋理我們就什么都不綁定,而是調(diào)用代碼讓其綁定自己的紋理。
總結(jié)
以上是生活随笔為你收集整理的opengl 深度详解_一步步学OpenGL(23) -《阴影贴图1》的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java phantomjs 截图_ph
- 下一篇: python导入同一文件夹下的类_pyt