文章出處

CSharpGL(44)用ShadowMapping方式畫物體的影子

在(前文)已經實現了渲染到紋理(Render To Texture)的功能,在此基礎上,本文記錄畫物體的影子的方式之一——shadow mapping。

下載

CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL

 

開始

如圖所示,在藍色背景下,有一個金色的茶壺(Teapot模型)和一塊灰色的地面(4個頂點組成的正方形),空中有一個立方體(代表光源的位置)發出光,照到茶壺和地面上,地面顯示出了茶壺的影子,茶壺身上也顯示出了壺柄和壺蓋的部分影子。這就是shadow mapping的效果。

本文僅以聚光燈類型的光源為例。其他類型的光源暫時先不做了,還有別的東西要弄。

原理

一個fragment為何會是陰影的所在地?因為有物體擋在了此fragment與光源之間。那么OpenGL如何反映這種(光源-物體-fragment)之間的遮擋關系?方法就是以光源的位置為攝像機的位置,渲染一遍場景,此時的深度圖就能夠反映這個關系:深度圖上的數據,就是距離光源最近的fragment的深度值(即,如果某fragment的深度值大于此值,那么他就在陰影中,否則就是被光源照射到了)。

下面是上圖的深度圖。(由于我設定茶壺是一直在旋轉的,所以茶壺的角度可能不吻合)

白色的部分,代表深度值為1,是最遠的,顏色越黑,代表深度值越接近0,即最近的。

我初次見到這種深度圖的時候,誤以為直接把這個圖貼到地面上就完事了,然而看看最后的效果圖,又不是這樣,十分不解。現在才知道不是直接貼,而是以此為依據,判定fragment是否在陰影內,從而計算此fragment的顏色值。

獲取Depth texture

為了獲取深度圖(Depth Texture),我們需要使用(上文)提到到Render To Teture技術:創建Framgbuffer,把Texture綁定到此Framebuffer的depth component上。然后以光源的位置為攝像機的位置,渲染整個場景。所以光源下的每個能夠產生陰影的結點,都要有這樣一個vertex shader(不需要fragment shadedr):

 1 #version 330
 2 
 3 uniform mat4 mvpMatrix;
 4 
 5 layout (location = 0) in vec4 position;;
 6 
 7 void main(void)
 8 {
 9     gl_Position = mvpMatrix * position;
10 }

 

哪里是影子?

然后就依據深度圖來判定fragment是不是在影子里。

Vertex shader

在shadow mapping的渲染過程中,由于需要和【攝像機在光源時,頂點在eye space里的坐標】比較,所以在vertex shader里要手動計算此坐標(shadow_coord)。

 1 #version 330
 2 
 3 uniform mat4 model_matrix;
 4 uniform mat4 view_matrix;
 5 uniform mat4 projection_matrix;
 6 
 7 uniform mat4 shadow_matrix;
 8 
 9 layout (location = 0) in vec4 position;
10 layout (location = 1) in vec3 normal;
11 
12 out VS_FS_INTERFACE
13 {
14     vec4 shadow_coord;
15     vec3 world_coord;
16     vec3 eye_coord;
17     vec3 normal;
18 } vertex;
19 
20 void main(void)
21 {
22     vec4 world_pos = model_matrix * position;
23     vec4 eye_pos = view_matrix * world_pos;
24     vec4 clip_pos = projection_matrix * eye_pos;
25     
26     vertex.world_coord = world_pos.xyz;
27     vertex.eye_coord = eye_pos.xyz;
28     vertex.shadow_coord = shadow_matrix * world_pos;
29     vertex.normal = normalize(mat3(view_matrix * model_matrix) * normal);
30     
31     gl_Position = clip_pos;
32 }

Fragment shader

在fragment shader里,其他方面與一般的光照計算相同,只有在判定此fragment屬于陰影內時,才會削弱光照對它的影響。sampler2DShadow是比sampler2D更適合做shadow mapping的紋理采樣器類型,它指向的,就是上一步得到的深度圖(depth texture)。可見,紋理是紋理,采樣器是采樣器,換個采樣器,仍舊可以對相同的紋理采樣,然而得到的數據是不同的。

 1 #version 330
 2 
 3 uniform sampler2DShadow depth_texture;
 4 uniform vec3 light_position;
 5 
 6 uniform vec3 material_ambient;
 7 uniform vec3 material_diffuse;
 8 uniform vec3 material_specular;
 9 uniform float material_specular_power;
10 
11 layout (location = 0) out vec4 color;
12 
13 in VS_FS_INTERFACE
14 {
15     vec4 shadow_coord;
16     vec3 world_coord;
17     vec3 eye_coord;
18     vec3 normal;
19 } fragment;
20 
21 void main(void)
22 {
23     vec3 N = normalize(fragment.normal);
24     vec3 L = normalize(light_position - fragment.eye_coord);
25     vec3 R = reflect(L, N);
26     vec3 E = normalize(fragment.eye_coord);
27     float NdotL = dot(N, L);
28     float EdotR = dot(E, R);
29     float diffuse = max(NdotL, 0.0);
30     float specular = max(pow(EdotR, material_specular_power), 0.0);
31     float f = textureProj(depth_texture, fragment.shadow_coord);
32     
33     color = vec4(material_ambient + f * (material_diffuse * diffuse + material_specular * specular), 1.0);
34 }

似乎忘了記錄一下如何實現經典的光照模型,等下一篇吧。

總結

最近突然發現了多次遍歷的妙用。

解析obj格式的模型文件,可以用多次遍歷的方式:

第一次遍歷,只記錄頂點數目、索引數目等統計值;

第二次遍歷,根據上次的統計值,直接創建固定長度的數組,既不浪費空間,又避免了動態數組的可能反復分配空間的問題;

第三次遍歷,對上次的數據計算法線;

第四次遍歷,計算模型的體積和中心位置。

渲染場景,也可以用多次遍歷的方式:

第一次遍歷,根據場景的樹結構,依次更新各個結點的位置;

第二次遍歷,對支持shadow mapping的結點,更新其depth texture;

第三次遍歷,渲染場景到窗口。

這樣代碼就能拆分開來,還能根據情況選擇是否使用其中某些遍歷步驟。比如解析obj文件,如果不需要計算法線,我可以跳過第三次遍歷;比如渲染場景,如果沒有shadow mapping結點,我可以跳過第二次遍歷。

聯想到編譯器也是采用的多次遍歷的方式,

第一次遍歷,詞法分析;

第二次遍歷,語法分析;

第三次遍歷,語義分析;

第四次遍歷,優化……

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()