轉(zhuǎn)自:http://class.gd/content/shadow-map%E9%98%B4%E5%BD%B1%E8%B4%B4%E5%9B%BE%E6%8A%80%E6%9C%AF%E4%B9%8B%E6%8E%A2%E2%85%A2
本文來源:
http://www.zwqxin.com/archives/opengl
上篇里的最后最后,提及了幾種比較有名的Shadow Map的延展技術(shù),Cascaded Shadow
Maps是其中比較近期才出現(xiàn)的,而且它引進(jìn)了Cascade(級(jí)聯(lián),層)這個(gè)概念,與另一個(gè)頗為我們中國(guó)人驕傲的名詞PSSM(Parallel-
split Shadow Maps)中的Parallel-split指的是同一個(gè)概念。事實(shí)上兩者的原理是基本一樣的。
它先在我們的視錐上動(dòng)手腳,用幾個(gè)與近遠(yuǎn)平面平行的截面把視錐分成幾份(Parallel-split);然后針對(duì)每一份,通過修改光源投影矩陣,使之后
生成的Shadow
Map中只有該份“Splited視錐”里的物體;這樣,在pass1階段就生成了幾張針對(duì)不同“Splited視錐”的Shadow
Maps,在渲染階段,依據(jù)像素深度就可以判斷該位置應(yīng)用哪張Shadow Map了。
這樣做的好處在上篇已經(jīng)講過了。在距離眼睛近的地方,應(yīng)用的是分辨率高的陰影圖,距離眼睛遠(yuǎn)的地方則是低分辨率。這樣是符合視覺特點(diǎn)的,而且沒有什么浪費(fèi)的地方。

如圖,假設(shè)光從視錐正上方射下來(其他方向同理),按CSM的意思,應(yīng)該把光源視覺下的投影面放在圖示位置(四條短的水平的線)。這里我把視錐分割成四
份,因此需要對(duì)應(yīng)的四張ShadowMap,與人看東西一樣,視像面越靠近陰影(假設(shè)位于被投影面,圖中長(zhǎng)水平線),看到的陰影越清晰。反映在生成陰影圖
階段,表現(xiàn)為具體caster(被光源直接照射的投射物表面)在光源投影面上占據(jù)的范圍大。假設(shè)陰影圖尺寸是固定的(譬如1024*1024),在第一個(gè)
“Splited視錐”和第四個(gè)“Splited視錐”里的投射陰影的物體[投射物]大小也相同(其陰影在實(shí)際世界里占地面積必然也相同),則其陰影在陰
影圖里占的像素?cái)?shù)會(huì)有很大差別(譬如前者占500,000個(gè),后者可能才占5000個(gè)),這就是分辨率的差異。最后把ShdowMap帖在場(chǎng)景里(假設(shè)在
世界空間下該種投射物的陰影應(yīng)該占100,000個(gè)像素),前者就會(huì)比后者效果好很多。(一個(gè)是需要進(jìn)行OverSampling,另一個(gè)就得進(jìn)行
UnderSampling。)所以越靠近眼睛的、越小的Splited視錐里的陰影越高“畫質(zhì)”,反之則越粗糙(但比起傳統(tǒng)Shadow
Map技術(shù)也許效果還好一點(diǎn))——而我們正希望要眼前的事物清晰,遠(yuǎn)處的事物模糊甚至不表現(xiàn)出影子也可以——CSM(或者說,PSSM)做到了。
重新回頭看看技術(shù)實(shí)現(xiàn)過程。這里有兩個(gè)主要的技術(shù)點(diǎn),一是“怎么分割視錐”,二是“怎么設(shè)置每個(gè)小視錐的光源投影矩陣”。
1. Cascade(Split)的準(zhǔn)則
從上圖和上分析可以看出,“Splited視錐”沿視線的長(zhǎng)度(Zfar -
Znear)應(yīng)該越分越大比較合理,指數(shù)增長(zhǎng)符合這個(gè)規(guī)律,但指數(shù)增長(zhǎng)一般太夸張了,所以配合一個(gè)線性增長(zhǎng)比較好。在PSSM里,這兩種分法叫
logarithmic split scheme和the uniform split
scheme,前者的表達(dá)式是經(jīng)過科學(xué)的推導(dǎo)的,這部分也是CSM/PSSM最數(shù)學(xué)的部分,在GPU
GEMS3里有詳細(xì)的推導(dǎo),或者你看PSSM推廣人Fan Zhang [HKUST]那篇"Hardware-Accelerated
Parallel-Split Shadow Maps." (IF YOU CAN FIND IT)也該有。它從Shadow-Map
Aliasing(dp/ds,單位陰影圖像素單位對(duì)應(yīng)的屏幕像素)的推導(dǎo)開始,找出能滿足使perspective
aliasing(由投影縮減效應(yīng)形成)在各個(gè)視錐里均勻分配的分割式。
后者只是一個(gè)線性式,但它的調(diào)和作用避免了“Splited視錐”的過小與過大,通過一個(gè)mix因子混合兩式子(我在應(yīng)用中默認(rèn)分配logarithmic split scheme的因子是0.75,余者0.25):

02 |
void CCascadingSM::ComputeSplits( float strength, float Dis_Near, float Dis_Far) |
04 |
float distance_scale = Dis_Far / Dis_Near; |
06 |
splitfrust[0].ResightNear(Dis_Near); |
08 |
float partisionFactor = 0.0; |
09 |
float lerpValue1 = 0.0, lerpValue2 = 0.0; |
12 |
for ( int i = 1; i < NumofSplits; ++i) |
14 |
partisionFactor = i / ( float )NumofSplits; |
16 |
lerpValue1 = Dis_Near + partisionFactor * (Dis_Far - Dis_Near); |
18 |
lerpValue2 = Dis_Near * powf(distance_scale, partisionFactor); |
21 |
SplitsZ = (1-strength) * lerpValue1 + strength * lerpValue2; |
23 |
splitfrust[i].ResightNear(SplitsZ * 1.002f); |
24 |
splitfrust[i-1].ResightFar(SplitsZ); |
27 |
splitfrust[NumofSplits-1].ResightFar(Dis_Far); |
2. Crop It !
針對(duì)每個(gè)光源投影矩陣進(jìn)行的調(diào)整,在CSM/PSSM里稱為Crop(這么有詩(shī)情畫意噶?)。這個(gè)過程其實(shí)很好理解的,我們?cè)谡障嗟臅r(shí)候,一開始要在
CCD液晶屏的畫面上把焦點(diǎn)確定吧——Cascaded Shadow
Maps技術(shù)中的光源就是照相者,光源的視像平面就是屏幕,我們是對(duì)每個(gè)“Splited視錐”都照一張相,因?yàn)檎盏氖莄asters,所以可以說是照人
物相片——把casters所在的“Splited視錐”(對(duì)應(yīng)人物背景)在光源投影空間的中心挪移到視像平面的中心,然后進(jìn)行光學(xué)變焦,使人物背景盡量
充滿屏幕,從而突出人物——casters,噢,不,應(yīng)說是shadows。
恩,這是個(gè)具有平移和縮放的線性變換——CROP
MATRIX,合適地構(gòu)造它,然后乘在光源投影矩陣前面(形成新的投影矩陣),就能完成匹配投影矩陣匹配“Splited視錐”的任務(wù)。假如目前處理第i
個(gè)分割視錐,生成CropMatrix[i],那么對(duì)場(chǎng)景坐標(biāo)系的變換就是:(CropMatrix[i] *
LightProjectMatrix) * LightViewMatrix * ModelMatrix *
pos。也可認(rèn)為(CropMatrix[i] * LightProjectMatrix)是二次投影,因?yàn)镃rop
Matrix實(shí)質(zhì)也是個(gè)投影矩陣,而且是個(gè)名副其實(shí)的Otho正交投影矩陣。
02 |
void CCascadingSM::ApplyCropProjectMatrix(CFrustum &frust) |
04 |
CVector3 maxFrustumCoord, minFrustumCoord; |
06 |
CMatrix16 CurrentMatrix; |
10 |
glGetFloatv(GL_MODELVIEW_MATRIX, CurrentMatrix.mt); |
13 |
GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMatrix); |
16 |
float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x); |
17 |
float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y); |
18 |
float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX; |
19 |
float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY; |
21 |
CropMatrix = CMatrix16(scaleX, 0.0f, 0.0f, 0.0f, |
22 |
0.0f, scaleY, 0.0f, 0.0f, |
23 |
0.0f, 0.0f, 1.0f, 0.0f, |
24 |
offsetX, offsetY, 0.0f, 1.0f ); |
28 |
glLoadMatrixf(CropMatrix.mt); |
30 |
glOrtho(-1.0, 1.0, -1.0, 1.0, -maxFrustumCoord.z, -minFrustumCoord.z ); |
CropMatrix簡(jiǎn)直就跟glOrtho生成的矩陣一模一樣,功用也一樣。只不過這里我沒有對(duì)Z坐標(biāo)進(jìn)行變換,因?yàn)榘阉唤o生成光源投影矩陣的
glOrtho了(反而它只變換Z坐標(biāo))。前面不是說把坐標(biāo)都變換到光源投影CLip空間后再提取AABB嗎,為什么就到光源視圖空間就比較了?因?yàn)檫@里
是平行光的投影,所以用的是正交投影glOrtho,在glOrtho中沒有對(duì)X,Y坐標(biāo)進(jìn)行變換(看看它的spec就知道了,-1與1為參數(shù)是不改變
X,Y數(shù)值的),所以兩個(gè)空間下的X,Y坐標(biāo)是一致的,而CropMatrix正是只變換X,Y坐標(biāo),所以實(shí)在沒必要多此一舉。
但有兩種情況是“需要多此一舉”的。一是光源為點(diǎn)光源且需要透視投影;二是在光源與視錐之間還有其他caster。對(duì)第二種情況尤其值得注意。看回我在文
章最上面放的自畫示意圖,有個(gè)打了X的地方,那里假設(shè)有只bird,那么它會(huì)否對(duì)地面產(chǎn)生陰影呢?——按照CSM基礎(chǔ)理論,不會(huì)!因?yàn)?
CropMATRIX修改后的光源投影平面已經(jīng)越過它了,已經(jīng)看不見它了——我們只能看見視錐里(更準(zhǔn)確說是視錐的AABB包圍盒里)的物體所留下的陰
影!解決法是把該物件bondingbox在光源視圖空間下的最大Z坐標(biāo)作為上述算法最后的minFrustumCoord.z,使光源投影平面恰在該位
置而不再下降。這樣做多了些麻煩,而且該“Splited視錐”對(duì)應(yīng)的Shadow
Map的分辨率會(huì)降低,物體離視錐越遠(yuǎn),分辨率下降越嚴(yán)重。所以,如非必要投射那樣的物體(或者部分穿出視錐之外的物體)的陰影,不必這樣做:
先計(jì)算普適意義下的光源投影矩陣和視圖矩陣(類似傳統(tǒng)SM那樣),用它們的積Light-ProjectView把各個(gè)小視錐變換到CLIP投影空間,用
同樣方法得到該空間下的包圍盒(特征向量maxFrustumCoord,
minFrustumCoord),這里繼續(xù)計(jì)算的Crop矩陣就需要用到Z值了,因?yàn)槲覀円薷钠渲械膍inFrustumCoord.z。讓它等于
-1——OPENGL在CLIP投影空間的最小坐標(biāo)值。沒錯(cuò),即使該物件在光源正體位置之上,也把它計(jì)算入要投影的物件集(casters)里(況且平行
光源本來該是無限遠(yuǎn)而不是在那個(gè)虛擬位置上的)。最后依然是:CropMatrix[i] *( LightProjectMatrix *
LightViewMatrix) * ModelMatrix * pos。
02 |
CVector3 maxFrustumCoord, minFrustumCoord; |
04 |
GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMV); |
06 |
minFrustumCoord.z = -1.0f; |
09 |
float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x); |
10 |
float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y); |
11 |
float scaleZ = 2.0f / (maxFrustumCoord.z - minFrustumCoord.z); |
12 |
float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX; |
13 |
float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY; |
14 |
float offsetZ = -0.5f*(maxFrustumCoord.z + minFrustumCoord.z) * scaleZ; |
17 |
CropMatrix = CMatrix16(scaleX, 0.0f, 0.0f, 0.0f, |
18 |
0.0f, scaleY, 0.0f, 0.0f, |
19 |
0.0f, 0.0f, scaleZ, 0.0f, |
20 |
offsetX, offsetY, 0.0f, 1.0f ); |
3. Cast 陰影
通過上面矩陣配合(0,1)映射矩陣之類的生成shadow maps后,這就來到第二PASS了,它與傳統(tǒng)Shadow
Map(Shadow
Map陰影貼圖技術(shù)之探Ⅰ)一樣,只是根據(jù)像素深度決定用哪張而已。注意,把視錐分割的是近/遠(yuǎn)平面,其值是距視點(diǎn)的距離,定義于視圖空間——把它變換到
眼睛的屏幕CLIP空間,就能在shader里“分割”像素深度,把像素都分到SplitNum個(gè)區(qū)域里(應(yīng)用中我取了4個(gè))。好了,接下來你知道怎么用
if-else來Cast 陰影圖了吧。
05 |
const float shadow_color = 0.3; |
06 |
const float depth_error = 0.005; |
08 |
uniform vec3 frustum_far; |
09 |
uniform sampler2DArray shadowmap; |
18 |
if (gl_FragCoord.z < frustum_far.x) |
22 |
else if (gl_FragCoord.z < frustum_far.y) |
26 |
else if (gl_FragCoord.z < frustum_far.z) |
32 |
vec4 shadowTexcoord = gl_TextureMatrix[index] * pos; |
36 |
if (shadowTexcoord.w != 1.0) |
38 |
shadowTexcoord = shadowTexcoord / shadowTexcoord.w; |
42 |
shadowTexcoord = 0.5 * shadowTexcoord + 0.5; |
45 |
float realDepth = shadowTexcoord.z; |
48 |
shadowTexcoord.z = float (index); |
51 |
float depth = texture2DArray(shadowmap, shadowTexcoord.xyz).x; |
55 |
float diff = depth - realDepth; |
59 |
diff = diff / depth_error + 1.0; |
61 |
return vec4(diff < 0.0 ? shadow_color : 1.0) ; |
最后是放出演示DEMO了吧

在該日志將展示DEMO并淺談一下CSM一些小細(xì)節(jié)的地方,包括caster-receiver-splitedFrustum組合生成的SCREEN DEPENDENT的crop矩陣。最后是這段時(shí)間個(gè)人學(xué)習(xí)Shadow技法的小小總結(jié)。