转载 用ShadowVolume画模型的影子
閱讀目錄(Content)
- Shadow Volume
- 包圍盒
- 動(dòng)態(tài)生成包圍盒
- 判斷
- 多光源下的陰影
- 總結(jié)
- 問(wèn)題
CSharpGL(48)用ShadowVolume畫(huà)模型的影子
回到頂部(go to top)Shadow Volume
在Per-Fragment Operations & Tests階段,有一個(gè)步驟是模版測(cè)試(Stencil Test)。依靠這一步驟,不僅可以實(shí)現(xiàn)渲染模型的包圍框這樣的實(shí)用功能,還能創(chuàng)造出一種渲染陰影的算法,即Shadow Volume算法。
用Shadow Mapping方法得到的陰影,在貼近觀察時(shí),會(huì)看到細(xì)微的鋸齒。這是因?yàn)樯疃染彺媸艿椒直媛实南拗?#xff0c;不可能完全精確地描述貼近觀察時(shí)的各個(gè)Fragment。但Shadow Volume方法得到的陰影是沒(méi)有這樣的鋸齒問(wèn)題的,如下圖所示:
對(duì)比(左)Shadow Mapping的陰影有鋸齒(右)Shadow Volume的陰影更平滑
如圖所示,左側(cè)的Shadow Mapping方法得到的陰影在犄角、舌頭和下巴部分可以看到比較明顯的鋸齒,而右側(cè)的Shadow Volume方法得到的陰影則十分平滑。這是由Shadow Volume的實(shí)現(xiàn)機(jī)制決定的。其機(jī)制概括起來(lái)就是,根據(jù)光源位置(或方向)和模型位置,動(dòng)態(tài)地生成一個(gè)不規(guī)則的包圍盒,將陰影部分包裹起來(lái),保證包圍盒內(nèi)部的Fragment不參與光照計(jì)算。上圖的包圍盒如下圖所示:
?
從6個(gè)視角觀察包圍盒
注意,這個(gè)包圍盒是實(shí)時(shí)動(dòng)態(tài)生成的,它會(huì)隨著光源位置(或方向)的變化而變化。而且,這個(gè)包圍盒是延伸到無(wú)限遠(yuǎn)的。這樣才能正確地渲染出陰影。
那么有2個(gè)主要問(wèn)題:首先,如何動(dòng)態(tài)生成這個(gè)包圍盒,而且能夠覆蓋無(wú)限遠(yuǎn)的范圍;然后如何根據(jù)包圍盒判斷Fragment是否參與光照計(jì)算。
為便于理解問(wèn)題,這里假設(shè)探討的模型都是由三角形網(wǎng)格拼接組成的。例如對(duì)于中國(guó)龍模型和Cube模型,其三角形網(wǎng)格結(jié)構(gòu)如下圖所示:
?
三角形網(wǎng)格組成的模型
可以看到,中國(guó)龍模型是由非常多的三角形網(wǎng)格拼接而成的,這利于觀察光照效果的真實(shí)感。Cube模型則僅僅由12個(gè)三角形拼接而成,這利于檢測(cè)程序的正確性。請(qǐng)讀者在隨書(shū)代碼中找到任意一個(gè)可以渲染中國(guó)龍模型的項(xiàng)目,為其添加PolygonModeSwitch開(kāi)關(guān),近距離觀察中國(guó)龍模型的三角形網(wǎng)格。
回到頂部(go to top)包圍盒
要找到一個(gè)模型在光源照射下的包圍盒,首先要找到在光源照射下的外圍輪廓(Outline)。輪廓的一側(cè)都是能被光源照射到的三角形,另一側(cè)都是不能被光源照射到的三角形(即處于陰影中),例如下圖所示:
?
(左)模型+輪廓線(中)模型(右)輪廓線
在此場(chǎng)景中,在中國(guó)龍模型的頭部方向上有一個(gè)聚光燈光源(下方的Cube模型也是)。圖左的白線勾勒出此時(shí)的輪廓線在模型上的位置,圖中為模型本身,圖右為隱藏了模型的輪廓線全貌。
輪廓是由一條條線段組成的。對(duì)于組成線段的每個(gè)頂點(diǎn),沿著從光源位置到頂點(diǎn)的方向,無(wú)限地延伸出去,就是要找的包圍盒的側(cè)面。然后再把輪廓中朝向光源的一側(cè)加上,再把無(wú)限遠(yuǎn)處封口,就得到了一個(gè)完整的包圍盒。完整的包圍盒如下圖所示:
?
從遠(yuǎn)到近觀察完整的包圍盒
如圖所示,完整的包圍盒從模型的位置,無(wú)限地延伸到了場(chǎng)景的邊緣。圖左為地面遮擋了一部分包圍盒的情形,圖右為隱藏了地面后顯示出的更完整的包圍盒。可以看到包圍盒在最遠(yuǎn)處仍然呈現(xiàn)出模型的輪廓的形狀,且包圍盒的近端和遠(yuǎn)端保持著對(duì)應(yīng)關(guān)系。
注意,包圍盒是一個(gè)完全封閉的盒子,其法線全部指向外側(cè)。這是在構(gòu)造包圍盒時(shí)特意設(shè)計(jì)的。這樣才能在后續(xù)的判斷過(guò)程中找到陰影。
回到頂部(go to top)動(dòng)態(tài)生成包圍盒
第一個(gè)問(wèn)題,如何判斷哪個(gè)頂點(diǎn)是在輪廓線上的呢?觀察下圖:
?
光線L照射到2個(gè)三角形上
如圖所示,兩個(gè)三角形的交界處,是一條共享的線段AB。三角形ABC正面向光源L,能夠被照射到,另一個(gè)三角形ABD則背面向光源L,即處于陰影中。那么這條共享的線段AB就應(yīng)當(dāng)成為輪廓線的一部分。如果兩個(gè)三角形同時(shí)正面向光源或同時(shí)背面向光源,那么它們之間的共享線段就不需要算到輪廓線里。要判斷一個(gè)三角形是否正面朝向光源,只需將光源的方向向量L與三角形面的法線向量N做dot乘法,如果結(jié)果為負(fù)數(shù),說(shuō)明是正面朝向光源;如果結(jié)果為正數(shù),說(shuō)明是反面朝向光源。
注意,這里使用的是三角形面的法線向量。Vertex Shader只能處理單個(gè)的頂點(diǎn)。要處理三角形面這樣的對(duì)象,就要使用Geometry Shader。為了得知一個(gè)三角形與周?chē)男┤切斡泄蚕磉?#xff0c;這里需要使用GL_TRIANGLES_ADJACENCY模式渲染的模型。這樣的模型有一個(gè)特點(diǎn),即每個(gè)三角形都包含了其周邊三角形的信息,如下圖所示:
?
GL_TRIANGLES_ADJACENCY模式的圖元
此圖展示了一個(gè)三角形網(wǎng)格模型的一部分(由6個(gè)頂點(diǎn)組成的4個(gè)三角形)。其中三角形ACE是Geometry Shader要處理的一個(gè)三角形,而三角形ABC、CDE和EFA是與三角形ACE有共享邊的三個(gè)三角形。這三個(gè)三角形被稱(chēng)為三角形ACE的鄰接三角形。向Geometry Shader依次傳入這6個(gè)頂點(diǎn),即可找到哪些邊構(gòu)成了模型的輪廓線。一般的,模型數(shù)據(jù)中是不包含鄰接信息的,這需要在加載模型后額外計(jì)算。
為實(shí)現(xiàn)Shadow Volume算法,找輪廓線的任務(wù)就是在Geometry Shader中完成的,其代碼如下:
?EmitOutline注意,代碼中的lightDir變量指的是從頂點(diǎn)到光源位置的向量,而圖示 7?13中的光源方向向量L是從光源到頂點(diǎn)的向量。兩者是相反的。因此在代碼中正面朝向光源的三角形面的法向量N與lightDir的dot結(jié)果為正數(shù)。
第二個(gè)問(wèn)題,有了輪廓線,將輪廓線的每一條線段都延伸出去,分別形成一個(gè)四邊形,就構(gòu)成了包圍盒的側(cè)面。所有正面朝向光源的三角形,就構(gòu)成了包圍盒的近頂。沿著包圍盒側(cè)面的方向,把各個(gè)近頂面分別推向無(wú)限遠(yuǎn)處,并且翻轉(zhuǎn)朝向,就構(gòu)成了包圍盒的遠(yuǎn)底。只需在生成輪廓線的代碼基礎(chǔ)上稍作修改,即可得到動(dòng)態(tài)生成包圍盒的Geometry Shader,代碼如下:
?EmitQuad上文提到,包圍盒的遠(yuǎn)底面是位于無(wú)限遠(yuǎn)處的。這是數(shù)學(xué)意義上的描述。具體到OpenGL,其實(shí)并不需要描述一個(gè)無(wú)限遠(yuǎn)的頂點(diǎn),只需要找到此頂點(diǎn)投影到近裁剪面上的投影位置即可。簡(jiǎn)單來(lái)說(shuō),只需將從光源到輪廓線上的頂點(diǎn)的向量作為xyz坐標(biāo),以0為w坐標(biāo),即可得到此投影位置。
從數(shù)學(xué)上理解此問(wèn)題需要一些晦澀的推導(dǎo)過(guò)程,這里從OpenGL Pipeline的角度來(lái)理解即可。一般的,OpenGL描述頂點(diǎn)位置都是用vec4(x, y, z, 1)。在Pipeline從Clip Space到NDC Space的變換過(guò)程中,會(huì)將所有頂點(diǎn)的xyz坐標(biāo)都除以w,所以vec4(x, y, z, w)、vec4(x/w, y/w, z/w, 1)和vec4(nx, ny, nz, nw)描述的都是同一個(gè)位置。試想,如果保持xyz的值不變,而不斷減小w的值,那么這個(gè)坐標(biāo)描述的位置會(huì)越來(lái)越遠(yuǎn);當(dāng)w減小到0時(shí),這個(gè)坐標(biāo)描述的就是一個(gè)無(wú)限遠(yuǎn)的位置。那么,沿著光源L到頂點(diǎn)的方向,走到無(wú)限遠(yuǎn)的那個(gè)位置,只能是vec4(LightDir, 0)。
回到頂部(go to top)判斷
使用Stencil Buffer和Depth Buffer實(shí)現(xiàn)陰影的渲染的過(guò)程如下偽代碼所示:
1 void ShadowVolume(Scene scene, ..)2 {3 // Render depth info into depth buffer.4 RenderDepthInfo(scene, ..);5 6 glEnable(GL_STENCIL_TEST); // enable stencil test.7 glClear(GL_STENCIL_BUFFER_BIT); // Clear stencil buffer.8 // Extrude shadow volume and save shadow info into stencil buffer.9 { 10 glDepthMask(false); // Disable writing to depth buffer. 11 glColorMask(false, false, false, false); // Disable writing to color buffer. 12 glDisable(GL_CULL_FACE); // Disable culling face. 13 14 // Set up stencil function and operations. 15 glStencilFunc(GL_ALWAYS, 0, 0xFF); 16 glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP); 17 glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP); 18 19 // Extrude shadow volume. 20 // Shadow info will be saved into stencil buffer automatically 21 // according to `glStencilOp...`. 22 Extrude(scene, ..); 23 24 // Reset OpenGL switches. 25 glEnable(GL_CULL_FACE); 26 glColorMask(true, true, true, true); 27 glDepthMask(true); 28 } 29 // Render the scene under the light with shadow. 30 { 31 // Set up stencil function and operations. 32 glStencilFunc(GL_EQUAL, 0x0, 0xFF); 33 glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); 34 35 // light the scene up. 36 RenderUnderLight(scene, ..); 37 } 38 glDisable (GL_STENCIL_TEST); // disable stencil test. 39 }Shadow Volume由3遍渲染完成。
第一遍渲染時(shí),在不考慮陰影的前提下正常渲染場(chǎng)景。此時(shí),Depth Buffer填充了正常的深度信息。這一次渲染的目的是準(zhǔn)備好這一深度緩存,渲染的顏色并不重要。因此可以用最簡(jiǎn)單的Fragment Shader,甚至不使用Fragment Shader。
第二遍渲染前,啟用模板測(cè)試,并按如下方式設(shè)置模板測(cè)試的函數(shù)和操作:
1 // Always pass stencil test. 2 glStencilFunc(GL_ALWAYS, 0, 0xFF); 3 // If depth test fails for back face, increase value in stencil buffer. 4 glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP); 5 // If depth test fails for front face, decrease value in stencil buffer. 6 glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);這里設(shè)置模版測(cè)試對(duì)于每個(gè)像素都是通過(guò)的,且通過(guò)后將對(duì)應(yīng)像素位置的模板緩存值設(shè)置為0。當(dāng)模板測(cè)試完成后,對(duì)于包圍盒背面的Fragment,如果深度測(cè)試失敗,那么模版緩存的值加1;對(duì)于包圍盒正面的Fragment,如果深度測(cè)試失敗,那么模板緩存的值減1。
這樣設(shè)置的結(jié)果是,位于包圍盒內(nèi)部的模型(或其一部分),包圍盒的背面的深度測(cè)試會(huì)失敗,所以此處的模板緩存值加1;包圍盒正面的深度測(cè)試會(huì)成功,所以對(duì)模板緩存無(wú)影響。比包圍盒更靠近Camera的模型(或其一部分),包圍盒的正面背面的深度測(cè)試都會(huì)失敗,所以此處的模板緩存值加1又減1,保持為0。比包圍盒更遠(yuǎn)離Camera的模型(或其一部分),包圍盒的正面背面的深度測(cè)試都會(huì)成功,所以此處的模板緩存值保持不變,即為0。而在包圍盒涉及不到的位置,模板緩存也保持不變,即為0。
也就是說(shuō),只有包圍盒內(nèi)部的模型(或其一部分)對(duì)應(yīng)的模板緩存值是大于0的,其它位置的模板緩存值都保持為0。而包圍盒內(nèi)部的模型(或其一部分)就位于陰影中。所以第二遍渲染的只有包圍盒,這樣就能區(qū)分出陰影部分,如下圖所示:
?
Shadow Volume判斷陰影
如圖所示,場(chǎng)景中有一個(gè)點(diǎn)光源L位于左上角,一個(gè)地板(Floor)上方漂浮著一個(gè)立方體(Cube)。光源L照射到Cube和Floor上,Cube投射出自己的陰影,這陰影由包圍盒描述處出來(lái)。圖中ABCD都代表Floor上的一點(diǎn)。A點(diǎn)位于包圍盒內(nèi)部,包圍盒的背面的深度測(cè)試會(huì)失敗,所以此處的模板緩存值加1;包圍盒正面的深度測(cè)試會(huì)成功,所以對(duì)模板緩存無(wú)影響。B點(diǎn)比包圍盒更靠近Camera,因此此位置上的包圍盒的正面背面的深度測(cè)試都會(huì)失敗,所以此處的模板緩存值加1又減1,保持為0。C點(diǎn)比包圍盒更遠(yuǎn)離Camera,包圍盒的正面背面的深度測(cè)試都會(huì)成功,所以此處的模板緩存值保持不變,即為0。D點(diǎn)與包圍盒的任何一部分都沒(méi)有交集,因此不受包圍盒影響,模板緩存在此位置的值保持不變,即為0。
包圍盒本身是一個(gè)模型,但并不存在于原本的場(chǎng)景中,所以在第二次渲染過(guò)程中要通過(guò)下述代碼來(lái)避免將其渲染到最終的場(chǎng)景中:
1 glDepthMask(false); // Disable writing to depth buffer. 2 glColorMask(false, false, false, false); // Disable writing to color buffer.這樣就保證了包圍盒不改變深度緩存,也不會(huì)寫(xiě)入顏色緩存。同時(shí),其他功能仍然能夠正常進(jìn)行。
為了保證包圍盒的正面背面都被渲染,需要禁用背面剔除功能:
1 glDisable(GL_CULL_FACE); // Disable culling face.第三遍渲染前,重新設(shè)置模板測(cè)試的函數(shù)和操作:
1 // Draw only if the corresponding stencil value is zero. 2 glStencilFunc(GL_EQUAL, 0x0, 0xFF); 3 // prevent updating to the stencil buffer. 4 glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);此時(shí)設(shè)置模板測(cè)試僅允許模板緩存值為0的位置通過(guò)。也就是說(shuō),只有在第二次渲染時(shí)位于包圍盒外部的模型(或其一部分)才會(huì)被渲染并可能影響到最后的Framebuffer。此時(shí)已經(jīng)無(wú)需(也不應(yīng))修改模板緩存的值,所以設(shè)置在任何情形下都讓模板緩存的值保持不變。
這時(shí),只需按通常的方式用光照模型渲染場(chǎng)景,即可產(chǎn)生即有光照又有陰影的最終效果。
回到頂部(go to top)多光源下的陰影
無(wú)論Shadow Mapping還是Shadow Volume都可以簡(jiǎn)單地應(yīng)用到有多個(gè)光源的場(chǎng)景中。類(lèi)似于多光源下的光照模型Blinn-Phong,只需分別對(duì)每個(gè)光源執(zhí)行一遍Shadow Mapping或Shadow Volume,并且用混合功能將各個(gè)光源的照射效果疊加即可。其偽代碼如下:
1 void MultipleLights(Scene scene, ..)2 {3 // render ambient color.4 5 foreach (var light in scene.Lights)6 {7 // preparation.8 9 glEnable(GL_BLEND); // enable blending. 10 glBlend(GL_ONE, GL_ONE); // add lighting info to previous lights. 11 12 // light the scene up with specified light. 13 RenderUnderLight(scene, light, ..); 14 15 glDisable(GL_BLEND); 16 } 17 }下圖展示了同時(shí)用紅綠藍(lán)三色光源照射模型的場(chǎng)景:
?
多光源照射的光和影(左)點(diǎn)光源(中)平行光(右)聚光燈
圖中用三個(gè)小球描述了光源的位置。對(duì)于平行光,小球描述的是光源的方向。
回到頂部(go to top)總結(jié)
本文介紹了Shadow Volume渲染陰影的方法。Shadow Volume的實(shí)現(xiàn)相對(duì)復(fù)雜,對(duì)模型的規(guī)范性有一定的要求,但是陰影的分辨率很高。如果將Shadow Mapping類(lèi)比作位圖,那么Shadow Volume可以類(lèi)比作矢量圖。
r? Shadow Mapping的思路是什么?
兩遍渲染:首先從光源位置渲染場(chǎng)景,得到深度緩存;然后依據(jù)深度緩存判斷Fragment是否位于陰影中。
r? Shadow Volume的思路是什么?
兩遍渲染:首先動(dòng)態(tài)生成陰影包圍盒,并設(shè)置模版緩存;然后依據(jù)模版緩存的狀態(tài)判斷Fragment是否位于陰影中。
r? 多光源的陰影如何實(shí)現(xiàn)?
依次對(duì)每個(gè)光源運(yùn)用Shadow Mapping或Shadow Volume算法。
r? Shadow Volume最可能的失敗原因是什么?
創(chuàng)建OpenGL Render Context時(shí)沒(méi)有指定創(chuàng)建模版緩存。
回到頂部(go to top)問(wèn)題
帶著問(wèn)題實(shí)踐是學(xué)習(xí)OpenGL的最快方式。這里給讀者提出幾個(gè)問(wèn)題,作為拋磚引玉之用。
*****************************************************************************************
博主筆記:陰影體的渲染算法中,我們沒(méi)看到陰影本身(黑色像素)是如何渲染的,只看到了場(chǎng)景本身的渲染。
那么陰影是如何出來(lái)的呢?我的理解是:glclear(gl_color_buffer)?默認(rèn)是黑色,同時(shí)渲染場(chǎng)景時(shí)使用stencilbuffer拋棄了對(duì)陰影中像素的處理,那么陰影像素就是gl_clear_color,為純黑色。關(guān)于這個(gè),可以參考:https://blog.csdn.net/jxw167/article/details/65435329
總結(jié)
以上是生活随笔為你收集整理的转载 用ShadowVolume画模型的影子的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【转】UNITY中相机空间,投影空间的正
- 下一篇: 图形学复习笔记