日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

转载 用ShadowVolume画模型的影子

發布時間:2025/3/18 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 转载 用ShadowVolume画模型的影子 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

閱讀目錄(Content)

  • Shadow Volume
  • 包圍盒
  • 動態生成包圍盒
  • 判斷
  • 多光源下的陰影
  • 總結
  • 問題

CSharpGL(48)用ShadowVolume畫模型的影子

回到頂部(go to top)

Shadow Volume

在Per-Fragment Operations & Tests階段,有一個步驟是模版測試(Stencil Test)。依靠這一步驟,不僅可以實現渲染模型的包圍框這樣的實用功能,還能創造出一種渲染陰影的算法,即Shadow Volume算法。

用Shadow Mapping方法得到的陰影,在貼近觀察時,會看到細微的鋸齒。這是因為深度緩存受到分辨率的限制,不可能完全精確地描述貼近觀察時的各個Fragment。但Shadow Volume方法得到的陰影是沒有這樣的鋸齒問題的,如下圖所示:

對比(左)Shadow Mapping的陰影有鋸齒(右)Shadow Volume的陰影更平滑

如圖所示,左側的Shadow Mapping方法得到的陰影在犄角、舌頭和下巴部分可以看到比較明顯的鋸齒,而右側的Shadow Volume方法得到的陰影則十分平滑。這是由Shadow Volume的實現機制決定的。其機制概括起來就是,根據光源位置(或方向)和模型位置,動態地生成一個不規則的包圍盒,將陰影部分包裹起來,保證包圍盒內部的Fragment不參與光照計算。上圖的包圍盒如下圖所示:

?

從6個視角觀察包圍盒

注意,這個包圍盒是實時動態生成的,它會隨著光源位置(或方向)的變化而變化。而且,這個包圍盒是延伸到無限遠的。這樣才能正確地渲染出陰影。

那么有2個主要問題:首先,如何動態生成這個包圍盒,而且能夠覆蓋無限遠的范圍;然后如何根據包圍盒判斷Fragment是否參與光照計算。

為便于理解問題,這里假設探討的模型都是由三角形網格拼接組成的。例如對于中國龍模型和Cube模型,其三角形網格結構如下圖所示:

?

三角形網格組成的模型

可以看到,中國龍模型是由非常多的三角形網格拼接而成的,這利于觀察光照效果的真實感。Cube模型則僅僅由12個三角形拼接而成,這利于檢測程序的正確性。請讀者在隨書代碼中找到任意一個可以渲染中國龍模型的項目,為其添加PolygonModeSwitch開關,近距離觀察中國龍模型的三角形網格。

回到頂部(go to top)

包圍盒

要找到一個模型在光源照射下的包圍盒,首先要找到在光源照射下的外圍輪廓(Outline)。輪廓的一側都是能被光源照射到的三角形,另一側都是不能被光源照射到的三角形(即處于陰影中),例如下圖所示:

?

(左)模型+輪廓線(中)模型(右)輪廓線

在此場景中,在中國龍模型的頭部方向上有一個聚光燈光源(下方的Cube模型也是)。圖左的白線勾勒出此時的輪廓線在模型上的位置,圖中為模型本身,圖右為隱藏了模型的輪廓線全貌。

輪廓是由一條條線段組成的。對于組成線段的每個頂點,沿著從光源位置到頂點的方向,無限地延伸出去,就是要找的包圍盒的側面。然后再把輪廓中朝向光源的一側加上,再把無限遠處封口,就得到了一個完整的包圍盒。完整的包圍盒如下圖所示:

?

從遠到近觀察完整的包圍盒

如圖所示,完整的包圍盒從模型的位置,無限地延伸到了場景的邊緣。圖左為地面遮擋了一部分包圍盒的情形,圖右為隱藏了地面后顯示出的更完整的包圍盒。可以看到包圍盒在最遠處仍然呈現出模型的輪廓的形狀,且包圍盒的近端和遠端保持著對應關系。

注意,包圍盒是一個完全封閉的盒子,其法線全部指向外側。這是在構造包圍盒時特意設計的。這樣才能在后續的判斷過程中找到陰影。

回到頂部(go to top)

動態生成包圍盒

第一個問題,如何判斷哪個頂點是在輪廓線上的呢?觀察下圖:

?

光線L照射到2個三角形上

如圖所示,兩個三角形的交界處,是一條共享的線段AB。三角形ABC正面向光源L,能夠被照射到,另一個三角形ABD則背面向光源L,即處于陰影中。那么這條共享的線段AB就應當成為輪廓線的一部分。如果兩個三角形同時正面向光源或同時背面向光源,那么它們之間的共享線段就不需要算到輪廓線里。要判斷一個三角形是否正面朝向光源,只需將光源的方向向量L與三角形面的法線向量N做dot乘法,如果結果為負數,說明是正面朝向光源;如果結果為正數,說明是反面朝向光源。

注意,這里使用的是三角形面的法線向量。Vertex Shader只能處理單個的頂點。要處理三角形面這樣的對象,就要使用Geometry Shader。為了得知一個三角形與周圍哪些三角形有共享邊,這里需要使用GL_TRIANGLES_ADJACENCY模式渲染的模型。這樣的模型有一個特點,即每個三角形都包含了其周邊三角形的信息,如下圖所示:

?

GL_TRIANGLES_ADJACENCY模式的圖元

此圖展示了一個三角形網格模型的一部分(由6個頂點組成的4個三角形)。其中三角形ACE是Geometry Shader要處理的一個三角形,而三角形ABC、CDE和EFA是與三角形ACE有共享邊的三個三角形。這三個三角形被稱為三角形ACE的鄰接三角形。向Geometry Shader依次傳入這6個頂點,即可找到哪些邊構成了模型的輪廓線。一般的,模型數據中是不包含鄰接信息的,這需要在加載模型后額外計算。

為實現Shadow Volume算法,找輪廓線的任務就是在Geometry Shader中完成的,其代碼如下:

?EmitOutline

注意,代碼中的lightDir變量指的是從頂點到光源位置的向量,而圖示 7?13中的光源方向向量L是從光源到頂點的向量。兩者是相反的。因此在代碼中正面朝向光源的三角形面的法向量N與lightDir的dot結果為正數。

第二個問題,有了輪廓線,將輪廓線的每一條線段都延伸出去,分別形成一個四邊形,就構成了包圍盒的側面。所有正面朝向光源的三角形,就構成了包圍盒的近頂。沿著包圍盒側面的方向,把各個近頂面分別推向無限遠處,并且翻轉朝向,就構成了包圍盒的遠底。只需在生成輪廓線的代碼基礎上稍作修改,即可得到動態生成包圍盒的Geometry Shader,代碼如下:

?EmitQuad

上文提到,包圍盒的遠底面是位于無限遠處的。這是數學意義上的描述。具體到OpenGL,其實并不需要描述一個無限遠的頂點,只需要找到此頂點投影到近裁剪面上的投影位置即可。簡單來說,只需將從光源到輪廓線上的頂點的向量作為xyz坐標,以0為w坐標,即可得到此投影位置。

從數學上理解此問題需要一些晦澀的推導過程,這里從OpenGL Pipeline的角度來理解即可。一般的,OpenGL描述頂點位置都是用vec4(x, y, z, 1)。在Pipeline從Clip Space到NDC Space的變換過程中,會將所有頂點的xyz坐標都除以w,所以vec4(x, y, z, w)、vec4(x/w, y/w, z/w, 1)和vec4(nx, ny, nz, nw)描述的都是同一個位置。試想,如果保持xyz的值不變,而不斷減小w的值,那么這個坐標描述的位置會越來越遠;當w減小到0時,這個坐標描述的就是一個無限遠的位置。那么,沿著光源L到頂點的方向,走到無限遠的那個位置,只能是vec4(LightDir, 0)。

回到頂部(go to top)

判斷

使用Stencil Buffer和Depth Buffer實現陰影的渲染的過程如下偽代碼所示:

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遍渲染完成。

第一遍渲染時,在不考慮陰影的前提下正常渲染場景。此時,Depth Buffer填充了正常的深度信息。這一次渲染的目的是準備好這一深度緩存,渲染的顏色并不重要。因此可以用最簡單的Fragment Shader,甚至不使用Fragment Shader。

第二遍渲染前,啟用模板測試,并按如下方式設置模板測試的函數和操作:

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);

這里設置模版測試對于每個像素都是通過的,且通過后將對應像素位置的模板緩存值設置為0。當模板測試完成后,對于包圍盒背面的Fragment,如果深度測試失敗,那么模版緩存的值加1;對于包圍盒正面的Fragment,如果深度測試失敗,那么模板緩存的值減1。

這樣設置的結果是,位于包圍盒內部的模型(或其一部分),包圍盒的背面的深度測試會失敗,所以此處的模板緩存值加1;包圍盒正面的深度測試會成功,所以對模板緩存無影響。比包圍盒更靠近Camera的模型(或其一部分),包圍盒的正面背面的深度測試都會失敗,所以此處的模板緩存值加1又減1,保持為0。比包圍盒更遠離Camera的模型(或其一部分),包圍盒的正面背面的深度測試都會成功,所以此處的模板緩存值保持不變,即為0。而在包圍盒涉及不到的位置,模板緩存也保持不變,即為0。

也就是說,只有包圍盒內部的模型(或其一部分)對應的模板緩存值是大于0的,其它位置的模板緩存值都保持為0。而包圍盒內部的模型(或其一部分)就位于陰影中。所以第二遍渲染的只有包圍盒,這樣就能區分出陰影部分,如下圖所示:

?

Shadow Volume判斷陰影

如圖所示,場景中有一個點光源L位于左上角,一個地板(Floor)上方漂浮著一個立方體(Cube)。光源L照射到Cube和Floor上,Cube投射出自己的陰影,這陰影由包圍盒描述處出來。圖中ABCD都代表Floor上的一點。A點位于包圍盒內部,包圍盒的背面的深度測試會失敗,所以此處的模板緩存值加1;包圍盒正面的深度測試會成功,所以對模板緩存無影響。B點比包圍盒更靠近Camera,因此此位置上的包圍盒的正面背面的深度測試都會失敗,所以此處的模板緩存值加1又減1,保持為0。C點比包圍盒更遠離Camera,包圍盒的正面背面的深度測試都會成功,所以此處的模板緩存值保持不變,即為0。D點與包圍盒的任何一部分都沒有交集,因此不受包圍盒影響,模板緩存在此位置的值保持不變,即為0。

包圍盒本身是一個模型,但并不存在于原本的場景中,所以在第二次渲染過程中要通過下述代碼來避免將其渲染到最終的場景中:

1 glDepthMask(false); // Disable writing to depth buffer. 2 glColorMask(false, false, false, false); // Disable writing to color buffer.

這樣就保證了包圍盒不改變深度緩存,也不會寫入顏色緩存。同時,其他功能仍然能夠正常進行。

為了保證包圍盒的正面背面都被渲染,需要禁用背面剔除功能:

1 glDisable(GL_CULL_FACE); // Disable culling face.

第三遍渲染前,重新設置模板測試的函數和操作:

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);

此時設置模板測試僅允許模板緩存值為0的位置通過。也就是說,只有在第二次渲染時位于包圍盒外部的模型(或其一部分)才會被渲染并可能影響到最后的Framebuffer。此時已經無需(也不應)修改模板緩存的值,所以設置在任何情形下都讓模板緩存的值保持不變。

這時,只需按通常的方式用光照模型渲染場景,即可產生即有光照又有陰影的最終效果。

回到頂部(go to top)

多光源下的陰影

無論Shadow Mapping還是Shadow Volume都可以簡單地應用到有多個光源的場景中。類似于多光源下的光照模型Blinn-Phong,只需分別對每個光源執行一遍Shadow Mapping或Shadow Volume,并且用混合功能將各個光源的照射效果疊加即可。其偽代碼如下:

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 }

下圖展示了同時用紅綠藍三色光源照射模型的場景:

?

多光源照射的光和影(左)點光源(中)平行光(右)聚光燈

圖中用三個小球描述了光源的位置。對于平行光,小球描述的是光源的方向。

回到頂部(go to top)

總結

本文介紹了Shadow Volume渲染陰影的方法。Shadow Volume的實現相對復雜,對模型的規范性有一定的要求,但是陰影的分辨率很高。如果將Shadow Mapping類比作位圖,那么Shadow Volume可以類比作矢量圖。

r? Shadow Mapping的思路是什么?

兩遍渲染:首先從光源位置渲染場景,得到深度緩存;然后依據深度緩存判斷Fragment是否位于陰影中。

r? Shadow Volume的思路是什么?

兩遍渲染:首先動態生成陰影包圍盒,并設置模版緩存;然后依據模版緩存的狀態判斷Fragment是否位于陰影中。

r? 多光源的陰影如何實現?

依次對每個光源運用Shadow Mapping或Shadow Volume算法。

r? Shadow Volume最可能的失敗原因是什么?

創建OpenGL Render Context時沒有指定創建模版緩存。

回到頂部(go to top)

問題

帶著問題實踐是學習OpenGL的最快方式。這里給讀者提出幾個問題,作為拋磚引玉之用。

  • 請在Github代碼中的Demos\LogicOperation項目中嘗試使用各種類型邏輯操作,觀察各自的效果。注意,需要將鼠標移動到Cube模型上才能看到邏輯操作的效果。
  • 任意選擇一個示例項目,或者新建一個項目,嘗試使用剪切測試(Scissor Test),觀察效果。思考剪切測試能夠幫助實現什么功能?
  • 關于模版測試的示例項目Demos\StencilTest中,Cube的包圍框的寬度隨Cube的原理而逐漸減小。請嘗試使用Shader來保證包圍框的寬度保持不變。
  • *****************************************************************************************

    博主筆記:陰影體的渲染算法中,我們沒看到陰影本身(黑色像素)是如何渲染的,只看到了場景本身的渲染。

    那么陰影是如何出來的呢?我的理解是:glclear(gl_color_buffer)?默認是黑色,同時渲染場景時使用stencilbuffer拋棄了對陰影中像素的處理,那么陰影像素就是gl_clear_color,為純黑色。關于這個,可以參考:https://blog.csdn.net/jxw167/article/details/65435329

    總結

    以上是生活随笔為你收集整理的转载 用ShadowVolume画模型的影子的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。