天天看點

Vulkan Cascade Shadow Map的故事

你隻需做你自己,做你想做的事,不要淪為人海中的滄海一粟。

——《沉默的多數派》

最近幾周,看了看知乎上關于陰影的文章,雖然我記得大二的時候好像也看過那本《Unity Shader入門精粹》裡的陰影方法,但是時間隔得太久了,幾乎都忘完了,并且也懶得去看英文的資料(雖然我也下載下傳了4、5篇英文的論文,哈哈哈哈),是以你懂得,知乎上什麼都有,直接在知乎大學修一下就好了。

主要就是實作了一個CSM,并且在CSM的基礎上,改了一個級聯的EVSM出來,先看看效果:

Vulkan-AirEngine内實作的的CSM與CascadeEVSM83 播放 · 0 贊同視訊正在上傳…重新上傳取消​

倉庫還是這個,實在是懶得再倒騰到新倉庫了。。。

FREEstriker/Air_TileBasedForward (github.com)​github.com/FREEstriker/Air_TileBasedForward

Vulkan Cascade Shadow Map的故事

Forward+和CSM

Vulkan Cascade Shadow Map的故事

Forward+和CSM

Vulkan Cascade Shadow Map的故事

CSM

Vulkan Cascade Shadow Map的故事

CSM

Vulkan Cascade Shadow Map的故事

Cascade-EVSM

Vulkan Cascade Shadow Map的故事

Cascade-EVSM

自我評價一下:勉強能看。

Vulkan Cascade Shadow Map的故事

和之前的場景比,把一個小闆子換成了25個巨大的闆子作為底面,還在除了中間的大底面上,每個底面上放了9個球體,用來驗證大場景遠處的陰影正确性。除此之外,把Forward+渲染器中之前可配置的流程都固定下來,AO效果使用的是SSAO,OIT用的是A-Buffer,陰影用的是CSM。并且,新添加了一個渲染器,用來在螢幕空間内可視化陰影的效果,友善Debug的時候快速的看到陰影的效果。

Shadow Map

Shadow Map的原理是非常簡單的,就是将燈光當成一個相機,進行Depth-Only的渲染,獲得一張陰影貼圖,之後在真正的相機進行渲染片元時,會通過轉換矩陣将片元的投影坐标轉換至燈光相機坐标系下的投影坐标,進行透視除法獲得uv之後,對陰影貼圖進行采樣,就可以獲得這個片元在燈光相機的視角下是否被遮擋了,被遮擋了就說明燈光照不到,這個片元就是黑色的,反之就是片元的顔色。

Vulkan Cascade Shadow Map的故事

老生常談的問題就是acne問題和peter-panning問題,分别可以使用bias和正面剔除來解決。

Vulkan Cascade Shadow Map的故事

acne問題産生的原因就是像素化深度值得問題,當單個陰影貼圖的像素在實際進行比較的過程中,會出現同時覆寫多個片元的現象,這就導緻渲染的結果充滿交替的黑白色條紋,雖然增加陰影貼圖的分辨率可以緩解,但顯然是不能夠完全解決的,隻要使用陰影貼圖,就會有這種問題。

這時候隻要在比較時,手動的添加一個偏移值bias,讓片元離得近那麼一丢丢,避免出現條紋現象。

Vulkan Cascade Shadow Map的故事

當然,當相機的平截頭體過大或陰影貼圖分辨率過低的情況下,就會出現非常差的效果,許多許多片元使用同一個陰影貼圖的像素點,滿眼盡是馬賽克,光栅化真是難高啊。

Vulkan Cascade Shadow Map的故事

馬賽克的問題解決起來有三種類辦法,一種是Fitting、一種是Warp方法,還有一種是Partition方法。這位佬寫的非常詳細易懂:

楊鼎超:圖形學基礎 - 陰影 - ShadowMap及其延伸194 贊同 · 12 評論文章正在上傳…重新上傳取消

Fitting方法就是使燈光相機的平截頭體盡可能的小,盡可能提高陰影貼圖的使用率,這麼做顯然需要有場景管理的元件來加速,才能較快的壓縮體積。雖然我之前也曾經想過要寫個松散八叉樹的場景管理,但是感覺挺麻煩的,就不了了之了,是以這種方法就隻是看了看而已。

Vulkan Cascade Shadow Map的故事

Warp方法針對的點是:相機近處的點會占用渲染結果更多的像素,而遠處的點隻會占用較少的像素,那麼在渲染陰影貼圖的時候,通過某種方法,對其進行扭曲,讓近處的占有更多陰影貼圖的像素,這樣近處的渲染效果就會變好一點,具體的方法有PSM和、LiSPSM。因為這類方法好像用的比較少,幾乎沒有遊戲用這種方法(也有可能是我沒找到),是以我也是就大概看了看,沒有實際實作出來。

Vulkan Cascade Shadow Map的故事

而Partition方法用的很多了,CSM友善又簡單,效果又好還不用推複雜的公式,我的數學能力,拉中之拉,唉,真實太适合我了,真好。

Vulkan Cascade Shadow Map的故事

這類方法就是把相機的平截頭體給分割了,一個子平截頭體用一個陰影貼圖,簡單又暴力,挺nb的。

Cascade Shadow Map

瞎哔哔

上面整那麼一大堆,其實我都沒幹過,就是看了看罷了,無中生有搞這麼長,挺累的。

其實CSM也沒有那麼困難,隻是說把平截頭體分割了而已,最後可能要對多張陰影貼圖進行混合,有點麻煩,但是不難。我是主要學習了一下這兩篇文章,看明白了自己才寫的,感謝下大佬:

我好飽啊:Cascade Shadow Map 實作記錄68 贊同 · 4 評論文章正在上傳…重新上傳取消

宋開心:用DX11實作Cascaded shadow map141 贊同 · 27 評論文章正在上傳…重新上傳取消

大佬們真強啊,哈哈哈哈,我也想當大佬。

雖然不太難,但我實作的時候也是因為手殘瘋狂踩坑,包括但不限于:改半天Shader沒反應,發現改錯檔案了;透視除法直接用View空間坐标做的;忘了View空間的Z坐标是負數;燈光相機的視椎體裁剪不小心用成了相機的相交檢測器。。。反正就是狀況頻出,挺離譜的。

流程圖就是下面這樣子的:

Vulkan Cascade Shadow Map的故事

計算平截頭體

這部分非常簡單,既然已經有了相機的相關資料矩陣,那麼很容易就可以使用NDC空間的八個角點,像在Shader裡面一樣就可以反算出來View空間下的角點坐标了。

Vulkan Cascade Shadow Map的故事

建構子平截頭體

我是使用了一個4級的CSM,并通過一個分割比例數組來确定各級占整個平截頭體的大小。

Vulkan Cascade Shadow Map的故事

分割比例數組

接下來就是使用這麼幾個比例,對前面的八個角點進行線性插值就可以得到各個子平截頭體了。

Vulkan Cascade Shadow Map的故事

為了防止之後采樣各個陰影貼圖的時候在邊界出現跳變,就需要将各個子平截頭體設定一部分重疊區域,我是使用了一個重疊比系數的變量來對其進行設定。重疊區域應該還可以避免由于精度誤差造成的邊界漏光問題(我推測的)。

Vulkan Cascade Shadow Map的故事

其實就是除了第一個子平截頭體的近平面都向相機移動一段距離罷了。

Vulkan Cascade Shadow Map的故事

移動每個近平面

計算包圍球

這一步就是對每個子平截頭體整一個包圍球,方案有很多,具體的可以看這篇文章:

zilch:Cascade Shadow進階之路78 贊同 · 1 評論文章正在上傳…重新上傳取消

我是使用了最大球形包圍盒,使用這種方案的一個隐藏好處是處理陰影閃爍的問題比較友善,因為這個包圍球尺寸是不變。

Vulkan Cascade Shadow Map的故事

具體的計算方法就是參考的上面的文章,可以自己看下。

計算轉換矩陣

因為涉及到将片元從相機的View空間轉換到燈光相機的Projection空間,是以需要算一個轉換矩陣,我是這樣計算的:

Vulkan Cascade Shadow Map的故事

燈光的投影矩陣很簡單,和正交相機一樣,相機的View空間到燈光的View空間可以使用lookat函數來計算,不過需要注意的是最好是再算一個燈光經過Model變換後的Up向量作為參數,因為我自己寫的時候就發現,當擋光方向和相機方向成直角的時候,如果使用vec3(0, 1, 0)來作為Up,那麼算出來的矩陣就會是NaN,就挺離譜的。

接着使用燈光Projection矩陣*轉換矩陣*相機View矩陣,就可以獲得從世界空間轉換到燈光相機的Projection空間的矩陣了。

渲染

Vulkan Cascade Shadow Map的故事

确定虛拟燈光相機位置

首先根據各個平截頭體的外接圓,以及一個補償距離來建立虛拟燈光相機,之是以使用補償距離來确定虛拟燈光相機的位置,是因為我沒有類似于八叉樹那樣的場景管理系統,可以快速的得到陰影投射物的最遠坐标,是以隻能通過手動設定的方法,保證陰影投射正确。(着實莫得辦法才能這樣)

有了虛拟燈光相機的位置,就可以建構平截頭體進行視椎體(其實是視方體)剔除了:

Vulkan Cascade Shadow Map的故事

建構虛拟燈光相機的視椎體相交檢測器

Vulkan Cascade Shadow Map的故事

進行剔除,并綁定資源

剔除完直接畫就好了,沒啥可說的:

Vulkan Cascade Shadow Map的故事

第一級

Vulkan Cascade Shadow Map的故事

第二級

Vulkan Cascade Shadow Map的故事

第三級

Vulkan Cascade Shadow Map的故事

第四級

看起來沒有問題。有了陰影貼圖,接下來在實際渲染的時候采樣就可以了。

需要用到這麼一些參數:

Vulkan Cascade Shadow Map的故事

thresholdVZ是子平截頭體的近平面和遠平面的View空間的Z坐标,而且由于是交錯分布的,是以就可以找到片元所在的一個或是兩個平截頭體。我将bias也分成了多份,近處的bias和遠處的bias應該是不同的,因為一個陰影貼圖的像素所占的世界坐标的大小是不同的,是以我感覺還是分開比較好。matrixVC2PL就是之前計算出來的轉換矩陣,将片元從相機的View空間轉換到燈光相機的Projection空間。後面兩個參數就是用來确定PCF采樣的。

Vulkan Cascade Shadow Map的故事

既然已經知道了各個平面的Z坐标,那麼拿片元的View空間的Z坐标比較一下就好了:

Vulkan Cascade Shadow Map的故事

注意View空間下Z坐标是負數

如果是-1,就說明在最近的平面和最遠的平面之外,否做就是右側的平面的索引值。如果這個值是偶數,那麼他就使用(cascadIndex + 1) / 2來使用對應平截頭體的資料;如果他是奇數,那麼說明這個片元處于兩個平截頭體的重疊部分,就再使用cascadIndex / 2使用另一個平截頭體的資料,最後在對兩個平截頭體進行混合就好了。

Vulkan Cascade Shadow Map的故事

而關于混合重疊部分的方法,就是獲得重疊部分的大小,通過所占的比例,線性混合就好了:

Vulkan Cascade Shadow Map的故事

這樣基本上就可以了,但是這樣的陰影是非常硬的,不好看,需要讓他軟一點才好看。

直接PCF,簡單暴力,我是直接用的box-filter:

Vulkan Cascade Shadow Map的故事

但box-filter肯定效果不是最好的,它必然是會出現這種條紋狀的現象的:

Vulkan Cascade Shadow Map的故事

首先就是因為光纖和球體外圍是相切的,這就導緻陰影貼圖會出現跳變,那麼采樣平均後就會出現這種條紋狀,相當于把跳變拉伸了。底部的陰影的邊緣也不是那麼完美,也是帶點紋的,沒辦法,畢竟是box-filter。考慮使用随機旋轉的泊松采樣,應該會改善這種效果,不過泊松生成有點麻煩,并且随機旋轉的部分寫SSAO時也寫了,就懶得改了。

Cascade Exponential Variance Shadow Map

找資料的時候還發現了一種EVSM的方法,好像效果也挺好的,正好前面也實作了CSM,幹脆就摸了個級聯的EVSM出來。

EVSM屬于是預先模糊的一類方法,還有ESM、VSM、LVSM等,具體的思想就是避免在渲染片元的時候多次和附近的深度貼圖的值進行比較獲得陰影值,而是通過近似函數或是機率的方式,将比較得過程分離,在渲染前統一進行預模糊,之後通過單次采樣就獲得軟點的陰影。

演變

先說下ESM,他就是通過納皮爾常數的幂來近似替換陰影比較的階躍函數,這樣直接使用 ec(x−d) (c為負數,x為片元深度值,d為預模糊的深度貼圖值)就可以得到陰影了。

Vulkan Cascade Shadow Map的故事

階躍和e的-80x次方

VSM用的是一種完全不同的思路,它是将模糊區域内的陰影值當做一個機率值,使用單邊切比雪夫不等式,獲得陰影貼圖的深度比片元深度更深的機率,機率越大,說明片元越亮,反之越暗。單邊切比雪夫的推導可以去B站上找一找。

Vulkan Cascade Shadow Map的故事

單邊切比雪夫,t為片元深度,mu為陰影貼圖一定區域内的均值,sigma是陰影貼圖一定區域内的标準差

具體的計算就是對ShadowMap進行采樣,記錄深度與深度的平方,接着進行模糊,這樣就可以得到一階矩 μ 和二階矩 μ2 ,之後就用一階矩和二階矩來計算方差,然後就使用上面的函數進行計算就可以了,非常巧妙。

而EVSM則是VSM的改進版,它主要針對VSM的漏光問題進行解決,考慮VSM的下圖場景:

Vulkan Cascade Shadow Map的故事

當A與B的距離非常大,這導緻上式的方差很大,然後均值又接近B,而C又很接近B,這就會導緻在渲染C的斜線區域時,上式計算出來的機率接近1,導緻出現漏光現象。燈光相機使用的投影矩陣是正交矩陣,是以陰影貼圖上的深度就是線性深度,是以考慮:如果使用某種函數,讓這個切比雪夫不等式在應用時,減小深度變化造成機率大幅變化的情況,是以使用了一個 ecx 函數,來對線性深度值進行擴充,将它擴充了以後,就可以減輕微小變化造成的影響。這就是所謂的EVSM方法。

但是在實際應用時,還是使用了另外一個函數 −e−cx 對其進行限制,避免擴充後造成的凹凸放大現象。

實作

是以這麼來說,EVSM的流程相比VSM,隻是将模糊的對象變為了 ecx 與 −e−cx ,其他都是與VSM一樣的。而VSM與普通的ShadowMap方法相比,也隻是增加了一階矩二階矩模糊和單邊切比雪夫不等式的計算,流程是很清晰的。

Vulkan Cascade Shadow Map的故事

和CSM相比,前面的都是一樣的,我将右面的前兩步分成了Blit和Blur兩部,使用不同的Shader,比較正常的:

Vulkan Cascade Shadow Map的故事

Blit

Vulkan Cascade Shadow Map的故事

Blur

已經預模糊過後,在渲染實際的片元的時候,就不用PCF進行多次采樣了,直接帶入公式計算就好了,其他的重疊混合之類的都是一樣的:

Vulkan Cascade Shadow Map的故事

代入公式

Vulkan Cascade Shadow Map的故事

混合

其實和CSM都是差不多的。但是實際的效果隻能說是一般,經過兩次高斯模糊之後,它并沒有變得非常軟,隻能說比PCF3*3差不太多,并且球與光線方向相切的部分出現了非常難看的鋸齒狀,我也沒太想明白這是怎麼個情況。但是我好像在哪裡看過一種說法是什麼陰影投射體和陰影接受體得是同一個?記不太清了,也找不到是在哪裡看到的了,可能有點道理,我也确實沒太想明白。可能不太适合自遮擋的情況吧(我猜的)。

Vulkan Cascade Shadow Map的故事

球面與光線切面處的鋸齒

我還發現,當沒有底面的時候,EVSM的效果非常差,因為深度值直接是1,放大之後在混合,幾乎就都是底面的值占主導,這就導緻馬賽克非常嚴重,結合上面的式子也大概可以推斷EVSM是對相對距離變化不大的場景是比較有效的(不負責推斷)。

Vulkan Cascade Shadow Map的故事

馬賽克

還有就是有的文章裡說RGBA16SFLOAT的紋理就足夠了,但是我實際測試的使用效果很差,必須得使用RGBA32SFLOAT的紋理才可以,2048*2048再加上級聯化和高斯模糊,幀率極低。

總結

寫了兩三周,效果就是這樣,流程麻煩,但是不算很難,CSM簡單粗暴,但是效果就是很好,EVSM雖然看起來很好,但是情況還是很多的,并且還需要考慮到模糊與級聯的消耗,是以實際上也不是那麼友善,是以還是使用CSM+PCF的方案簡單些,并且不用考慮非常複雜的情況。

這幾天突然覺得,寫過的這些東西的原理雖然都比較簡單,但是和那種效果好的實作比起來還是雲泥之别,有非常多的細節需要注意與優化,還是挺困難的。

其實我還看了看PCSS的方法,不過實在懶得去寫了,一直寫一個還是挺枯燥的。下面可能會去看看PBR、球諧光照啥的,整個光照探針啥的?還想提升一下繪制的效率,看看Unity的合批到底在幹啥,簡單整個合批的功能到AirEngine裡,最後還想封成DLL,用AirEngine的支援做個簡單的FPS的DEMO出來。智能指針可能一堆好好想想怎麼才能放到裡面,之前一直都是純手動管理,感覺還是得用下智能指針,現在有地方的記憶體肯定已經洩露了。。。

後記

原神3.0版本都快結束了,我都還沒怎麼探索地圖,隻是把主線過了過,雕琢童心一點沒做。。。劇情還是挺有趣的,迪希亞也挺好看的,哈哈哈哈。希望過兩天的3.1加大力度。

Vulkan Cascade Shadow Map的故事

斯巴拉西

好久沒在宿舍做飯了,回來想想整個什麼活兒,還是挺想試試海鮮飯的。

最近開始試着寫力扣了,雖然算法都很妙,但就是刷不動,沒有動力,隻能說:“霓佳達黴達,霓佳達黴達”。。。

Vulkan Cascade Shadow Map的故事

霓佳達黴達

想吃廣式早茶的蝦餃了,是以周四和aty一起去Kevin' lab去整點漢堡吃,那天正好漢堡買一送一,哈哈哈哈,吃爆。

口掃的新裝置又來了,得開始幹點活兒了,難頂。

想擺,但是擺不動。。。

最近一直在聽欅坂46的歌,真好聽,《沉默的多數派》、《不協和音》、《黑羊》、《避雷針》都超好聽,欅共和果的live也超好看,平手友梨奈真帥啊哈哈哈哈,那首《渋谷からPARCOが消えた日》的氣場太足了,可惜就是早就解散了,這回又沒吃上熱乎的。。。

Vulkan Cascade Shadow Map的故事

繼續閱讀