本篇文章主要針對《OpenGL 4 Sharding Language Cookbook》一書中第七章——Shadow的第四節Creating soft shadow edges with random sampling解釋而得。
鋸齒問題(aliasing)
基於shadow map實現陰影效果有很多不是那麼令人滿意的地方,其中一個就是鋸齒問題(aliasing)。出現鋸齒的原因是因為shadow texture的大小往往小於螢幕大小,當把shadow texture渲染到螢幕上時,那些多餘的畫素就會顯現出來鋸齒。
最基本的shadow map的思想,我想大家都知道,就不說了。它在最後會根據shadow texture上的深度值來判斷當前要渲染的fragment是否在陰影裡,如果是,那麼它的visibility為0,否則就是1。這樣0和1的突變在影像邊緣處就表現為鋸齒。
PCF 方法
為了解決這個問題,一種通常的方法是使用PCF(percentage-closer filtering)。它的基本思想就是避免visibility從0到1的突變,它的基本實現是,當把某一fragment轉換到陰影空間(也可以說是shadow texture上對應的某一個畫素)時,visibility的取值由以該畫素為中心的某一區域內一些取樣點的visibility乘以它們的相對於中心點的百分比來決定的。當這個區域越大,取樣點越多,它的邊緣模糊效果就越好。通常簡單的實現是,在生成shadow texture的時候採用線性插值,並在渲染時判斷某fragment是否在陰影中時,對其在shadow texture上對應的畫素的周圍固定的若干畫素(如取其左上、右上、左下、右下四個畫素)進行取樣,最後取平均值來模擬實現soft shadow。
但這個方法有幾個缺點,一個是得到的邊緣的blur效果不是那麼明顯,它是以取樣區域的大小和取樣點的數目決定的,但這勢必會帶來效能上的下降;另一個是對於一些完全在陰影裡或者完全在陰影外面的點來說,這些取樣完全是浪費的,因為所有采樣點肯定要麼是1,要麼是0。而隨機取樣就是為了解決這兩個問題而出現的。
隨機取樣
顧名思義,它的取樣是隨機的,如下圖所示。
上圖顯示的是一張shadow texture,上面的每一個畫素都對應了一個visibility,取值為0或1。我們假設當前需要處理的fragment對應到shadow texture上後是十字架中心的那個畫素點(下面簡稱為中心點)。現在我們的取樣點來自它周圍那些畫“X ”的所有點,最後,該fragment的visibility就是這些取樣點的visibility的均值。現在的問題是,這些取樣點紋理座標應該如何確定。我們假設中心點的紋理座標是知道的,那麼問題轉化為,如何確定這些取樣點的紋理座標相對於中心點的偏移量。還有一個問題,就是對於不同的中心點,它們對應的取樣點的偏移量是否一樣。這裡我們沒有真正完全隨機,也就是說,我們有一些候選的隨機座標組(這些座標在一開始是隨機生成的,每一組包含了隨機取樣所需的所有采樣點偏移座標),當需要取樣時,我們從這些候選的隨機座標組中選擇一組進行計算。當然,我們可以完全隨機,也就是說計算每一箇中心點的visibility時,它的所有采樣點都在一定區域內隨機選取,但是這樣的代價是巨大的,因為每個fragment都需要重新計算取樣點的座標偏移。
再看這張圖,我們發現所有的取樣點包含在一個圓內,並且被分為了8個區域,每個區域隨機取一個點,這就是我們演算法的基本思想。這裡我們引入兩個變數,sampleU和sampleV,sampleU表示的圓被直線分割的區域數,sampleV表示圓環的個數,那麼取樣點的個數就是sampleU*sampleV。拿上面這張圖來說,它的sampleU和sampleU都是4,而下面這張圖sampleU為8,sampleV為6,所以它共有8*6 = 48個取樣點。(好吧,請原諒它這麼醜。。)
前面所過,我們需要事先生成一些候選的取樣偏移座標組,然後把它們儲存起來,在計算時再讀取它們。這是通過一張三維紋理貼圖來實現的。我們在引入一個vec3型別的變數OffsetTexSize,它定義了該三維紋理的width、height以及depth的大小,那麼width*height就是我們候選座標組的個數,而depth的值是sampleU*sampleV除以2。又沒懂吧,我們看一下三維紋理的儲存。
下圖中的s t r分別對應我們之前說的width height 和depth,我們可以理解為,每一個小正方體儲存了一個型別為vec4值,也就是該三維紋理座標為(s, t, r)的點對應的值。而類似圖中紅色部分的一組正方體則儲存了一個候選座標組。這樣的一組小正方體組共有width*height組,因此候選座標組的個數為width*height。那麼,depth值(也就是每一組小正方體的個數)不是因為等於sampleU*sampleV也就是取樣點的個數嗎?為什麼是等於取樣點總數的一半呢?這是因為,我們之前說過每個小正方體儲存的是一個vec4型別的資料,一個vec4資料可以儲存兩個座標,因此一個小正方體內就可以儲存兩個取樣點偏移座標,所以只需一半就可以儲存所有的取樣點偏移座標。
這樣,座標組的儲存問題就解決了。剩下的問題是,如何生成這些座標組。我們依舊假設sampleU=sampleV=4,那麼其中一組偏移座標的生成過程如下圖。
對於座標為(u,v)的取樣點,轉換到最右圖中,對應的就是從外往裡數第v層圓環、從x軸正方向逆時針數第u個小區域內的點。它的計算公式為:
其中Wx和Wy是真正偏移座標。
該三維紋理生成過程的C++程式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
void SceneJitter::buildJitterTex() { int size = jitterMapSize; int samples = samplesU * samplesV; int bufSize = size * size * samples * 2; float *data = new float[bufSize]; for( int i = 0; i < size; i++ ) { for(int j = 0; j < size; j++ ) { for( int k = 0; k < samples; k += 2 ) { int x1,y1,x2,y2; x1 = k % (samplesU); y1 = (samples - 1 - k) / samplesU; x2 = (k+1) % samplesU; y2 = (samples - 1 - k - 1) / samplesU; vec4 v; // Center on grid and jitter v.x = (x1 + 0.5f) + jitter(); v.y = (y1 + 0.5f) + jitter(); v.z = (x2 + 0.5f) + jitter(); v.w = (y2 + 0.5f) + jitter(); // Scale between 0 and 1 v.x /= samplesU; v.y /= samplesV; v.z /= samplesU; v.w /= samplesV; // Warp to disk int cell = ((k/2) * size * size + j * size + i) * 4; data[cell+0] = sqrtf(v.y) * cosf( TWOPI * v.x ); data[cell+1] = sqrtf(v.y) * sinf( TWOPI * v.x ); data[cell+2] = sqrtf(v.w) * cosf( TWOPI * v.z ); data[cell+3] = sqrtf(v.w) * sinf( TWOPI * v.z ); } } } glActiveTexture(GL_TEXTURE1); GLuint texID; glGenTextures(1, &texID); glBindTexture(GL_TEXTURE_3D, texID); glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA32F, size, size, samples/2, 0, GL_RGBA, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); delete [] data; } // Return random float between -0.5 and 0.5 float SceneJitter::jitter() { return ((float)rand() / RAND_MAX) - 0.5f; } |
其對應的fragment shader中的關於陰影計算的部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
void shadeWithShadow() { vec3 ambient = Light.Intensity * Material.Ka; vec3 diffAndSpec = phongModelDiffAndSpec(); ivec3 offsetCoord; offsetCoord.xy = ivec2( mod( gl_FragCoord.xy, OffsetTexSize.xy ) ); float sum = 0.0; int samplesDiv2 = int(OffsetTexSize.z); vec4 sc = ShadowCoord; for( int i = 0 ; i < 4; i++ ) { offsetCoord.z = i; vec4 offsets = texelFetch(OffsetTex,offsetCoord,0) * Radius * ShadowCoord.w; sc.xy = ShadowCoord.xy + offsets.xy; sum += textureProj(ShadowMap, sc); sc.xy = ShadowCoord.xy + offsets.zw; sum += textureProj(ShadowMap, sc); } float shadow = sum / 8.0; if( shadow != 1.0 && shadow != 0.0 ) { for( int i = 4; i < samplesDiv2; i++ ) { offsetCoord.z = i; vec4 offsets = texelFetch(OffsetTex, offsetCoord,0) * Radius * ShadowCoord.w; sc.xy = ShadowCoord.xy + offsets.xy; sum += textureProj(ShadowMap, sc); sc.xy = ShadowCoord.xy + offsets.zw; sum += textureProj(ShadowMap, sc); } shadow = sum / float(samplesDiv2 * 2.0); } FragColor = vec4(diffAndSpec * shadow + ambient, 1.0); // Gamma correct FragColor = pow( FragColor, vec4(1.0 / 2.2) ); } |