塞爾達風之杖渲染解析

霧


通過線框繪製模式可以看出“霧”是由幾個緩慢移動的自旋透明片構成(始終繫結在攝像機上),很尋常的做法。去掉紋理過濾後會更容易看出來。

雲


雲實際實際上是Billboard片,出現時有縮放和透明度變化做過渡。
但是這樣做只能顯示雲的側面貼圖,無法處理雲在頭頂的情況。所以遊戲裡視角通常是不能朝上看的。
但在第一人稱視角時必須允許朝上看,所以它做了以下處理:
- 所有Billboard片的方向並不是固定朝上,而是朝向頭頂中心點。朝上看的時候,所有云都指向天空中心,這樣就不會出現視野旋轉而云不轉的情況。
- 但云在接近天空中心的時候朝向不對的問題依然不能解決,而且通過中心的時候還會旋轉180度。所以它選擇讓雲接近中心時執行消隱邏輯,任何時候頭頂區域註定沒有云存在(即使是暴雨天氣)。


光


眾所周知,點光源的渲染成本遠高於平行光。
風之杖的所有點光源都能照亮場景,並體現出點光源的球形範圍特徵,更重要的是,它並沒有燈光數量上限(第二張圖兩個燈光區域疊加了)。
雖然點光源說起來也就是逐畫素計算和燈光的距離,算一次向量自點乘,但在場景光源較多的時候,剔除掉範圍外的光源也需要很多計算——而且實際上是做不到的,因為塞爾達每個Renderer都很大,只有不同Renderer才能選擇不同的光源組,然後又不可能用延遲渲染或者Tile-base按螢幕範圍拆分。
風之杖用的是一個很hack的做法。它的每個光源都是一個多邊形的球體。

然後設成Cull Front(正面剔除),以及ZTest Creater(被遮擋時才顯示),就成了這樣:

所以它的光照投影才不是個正圓,而是個微妙的Low Poly風格多邊形。這並不是故意而為之,單純是不想給這個光照球太多頂點。這樣做並不用專門剔除光照,也不會有任何額外繪製,但表現力就到此為止了,最多也就是用多個不同大小的光照球疊加來制照一些“光照邊緣”,因為在光照球的shader裡其實是不知道牆面和球的交點在哪的。

3個不同大小的光照球疊加
但是如果有不透明物體的深度快取,就能在螢幕上求出其交點,換算回世界座標並和光照中心求距離,並算出正確的點光源光照來。而這其實就是延遲渲染中對光照部分的處理,所以現在看也不完全是個hack的技術了。
此外,人物的光照和投影為求效率也使用的是平行光。對於多光源的處理,它採取的是接近後“捕獲”的做法,在公共區域用的則是最後一個接觸的光源,在光源切換的時候是直接對兩個光源的位置做插值,以避免切換時的突變。(請仔細觀察人物投影方向和光照方向變化的瞬間)



影
罐子和金幣等物體的影子採用了常見的透明圓片的設計(角度由下方碰撞箱的法線決定),所以可以看到這個明顯的顯示錯誤:

但是從上面看的時候,超出平臺的陰影卻能被隱藏。

由下圖隱藏了一部分的陰影可以判定,這個陰影實際上是通過Offest將自身的深度檢測向後位移了一段距離,然後設定為ZTest Greater(被遮擋)才允許顯示。這是個相當“實用”的貼花技巧。

至於動態陰影部分,




個人認為是每個人物單獨生成ShadowMap,然後從腳底附近的簡化Mesh碰撞箱篩選三角形動態建立Receiver(依據是與人的距離,法線與光照方向的夾角,所以從崖上往下的陰影無法在懸崖上顯示出來)。
因為是每個人物單獨生成ShadowMap,每張貼圖都可以比較小,記憶體佔用較小。但就算只渲染一個人,這張貼圖依然精度極低,鋸齒不明顯是對邊緣半透部分step的結果。

最終鋸齒部分近似為斜線
這個遊戲還有個特殊的設計:一旦人物懸空,或者在梯子和掛邊的時候,陰影就會固定到人物正下方。個人覺得這是為了表示人物的落點位置,單純是為了遊戲性考慮。但這樣的設計確實也方便了Receiver的篩選,遠處的物體將永遠不可能被篩選到,而且也避免了ShadowMap精度低,拉長後過於明顯的紋理走樣問題。

雨

相比大片紋理產生的overdraw,大量粒子生成的多邊形更加合算。雨實際上是由大量狹窄雨點三角面渲染而成的。
但由於雨點的下落速度一致,並不需要每個雨點單獨作為粒子,將十幾個雨點繫結成一個整體,隨機水平位置生成向下位移即可。

除了雨點本身外,還需要生成落在地面濺起的水花。用物理檢測實現是不可能的。通過反覆觀察水花生成的“錯誤”,風之杖的水花應該只是簡單地在低於視角的範圍隨機生成(高於視角看到的是Mesh背面,即使生成了也看不到),但是用的是ZTest Greater,也就是隻有當水花被建築遮擋的時候才會顯示。這樣水花就不會出現在空中。
但是當地面高低差較大的時候,由於水花範圍沒有那麼廣,無法落在建築下方,就無法顯示出來。
此外,這也會錯誤地導致牆面上出現水花,但因為牆壁的水花和視線幾乎平行,是一個極扁的圓,錯誤生成了也不是很明顯。
氣流扭曲

風之杖中廣泛使用了這個效果。做法很常規:在繪製完其他全部物體後,Grab Pass,最後繪製扭曲部分(不用Grab Pass直接取上一幀影象會取樣扭曲效果本身,導致越偏越遠)


扭曲效果的本體是一個普通的半透圓片貼圖粒子,除了透明部分外,色彩部分直接用的Grab Pass的貼圖再加一個噪波貼圖位移產生扭曲。並沒有什麼可說的。


但是在一些特殊情況下,“全屏”+“高速”,它則用了一個更近似的方案

GrabPass還是依舊,但這個效果並沒有用噪波紋理,甚至沒有用透明紋理,而是瘋狂甩出了大量高速移動的帶Grab貼圖的不透明粒子。

偏移攝像機後
取一幀檢視

它只是稍微偏移了下采樣的座標,然後靠不透明片隨機疊加,單幀的效果其實有一定破綻,但在連續畫面中是看不出來的。而因為不透明物體重疊會正常pixel kill,效能不差。
非要這樣做而不是用正常的後處理,應該主要還是為了體現出氣流運動的方向,用噪波的效果未必比這個好。運算量也確實要少一些,就像上圖的情況,其實有一部分螢幕根本沒繪製,而後處理至少要繪製一屏。省掉的噪波紋理那方面就更不用說了。
岩漿

這個岩漿效果的特點是:有實際高度變化,且足夠隨機(並且符合岩漿的特徵)
有經驗的人很快能看出,岩漿的顏色=水面高度(取世界座標Z作為依據lerp),不需要紋理。剩下的問題就是如何生成這樣的一個岩漿高度場。
正弦波疊出來的都比較接近波浪,而岩漿池的波形特點則是“沸騰”,簡而言之是個混沌系統,這是簡單的週期波模擬不出來的。雖然FFT理論上可能能模擬出來(但這就扯遠了)
實際上是這樣的:

看不明白就再放一個比較“稀疏”的例子


說白了,還是隨機“粒子”。
每個粒子是個兩層的8邊形,中間凸起。

隨機位置生成粒子後,從水平面下凸起來,然後再沉回去,作一個週期,然後移動到下一個隨機地點再次出現。
粒子依然是不透明物體以減少OverDraw,因為顏色是由高度決定的並不需要半透過度,產生和消失都在“水面”以下,也不需要半透漸隱。
疊加得越多,效果越自然。
頂點數看上去很誇張,但也只是看上去而已(線段長,交叉多)。實際上一個粒子也就16個頂點,而一個湖有50個粒子效果就已經不錯了,隨便一個水面需要的頂點數其實很輕鬆就能超過它。
效率問題反而在OverDraw上,雖然是不透明物體,依然還是有概率重複計算的。按Z排序後能緩解,但邊界處依然存在。
不過基本沒啥問題的,畢竟風之杖只是個NGC遊戲,這個能跑還有啥不能跑。
很容易聯想到,是否普通的水體,或者海是否也能用類似的方法實現……
理論上是可以的,只是需要處理法線連續,所以必須先把高度場渲染到紋理處理,然後再對映到網格上。
神海4用到的Wave Particles聽說就是類似的技術。
水體
水的部分內容較多,分成多個小節描述
(一)邊緣
水是透明的,人對水的感知更多依附於水與其他物體的交介面。

這個遊戲中大部分水都是一張純藍色的貼圖,然後在邊緣處單獨拉一個Renderer,使用特殊材質,拉好UV。

使用兩張貼圖,4次取樣。同種貼圖UV展開方向是相反的,相加後saturate,這樣出來的紋理看上去並沒有那麼重複。

白沫

深色倒影
(二)海浪拍岸

這裡取了個巧,任何位置的拍岸節奏都是一樣,只有UV x軸的步進速度不同。
海岸分3層,由下至上是:
1.波浪退去時潮溼的部分:

UV並不是位移,而是直接貼邊對y軸縮放,y軸畫素拉伸產生的模糊模擬了溼邊的擴散。
2.有實體的海浪,在圖示範圍正弦擺動,一個完整週期要擺動兩次。


這個波還有個不易察覺的細節:

接近岸邊倒極限的時候會顯示一個水面紋理,二次取樣,相反方向運動

使用的紋理
除了提供細節外,還提供了一點軟邊緣的效果:接近岸邊的部分變亮了,更像沙灘的顏色。
3.從深處來的浪花:看著像海浪,其實只有白沫,看著同時存在兩個波,其實是通過repeat方式複製出來的。

這個浪花一直保持勻速推進。
整個設計巧妙的地方在於,兩個波有一段時間是重合的。而第二種波的頻率比第三種大,它推到最內側之後,還會時間回來,再去接下一個波。重合期間由於一個是勻速,一個是正弦波,所以會錯開一點,使得白沫的變化更多。
正因為這個波只有白沫沒有藍色的海水部分,才能和下層的白沫重合在一起。
不過回退的時候處理不好就會出現這個狀況:

波2回退的時候,波3還沒有完全消失
波3的覆蓋範圍需要比波2稍短一些。波3的白沫需要在波2回退之前滾動出網格外。
(三)海平面交界線

這是不太容易注意到但是非常重要的細節。海水靠近地平面的地方應該變亮,並逐漸過渡到接近天空的顏色,也就是一種類似菲涅爾的現象。
我們肯定不願意在frag上正常算一次viewDir點乘法線。但這整個海只是一個Quad,也沒法交給頂點去算,那怎麼才能讓遠處的顏色發生變化呢?
用遠景霧確實可以,但這還是要算。

調整鏡頭後,我們可以俯瞰整個海的Quad的形狀。可以看到這個Quad並不是純色,並且其影象在任何方向觀察都一樣,所以它很可能是貼了個這樣的貼圖:

放大到足夠大,以接近水平的角度觀察,視覺上就產生了這樣的效果。


(四)波浪
在大陸附近,海面是由一個Quad組成的,這多半是為了效能。為了表現海浪,風之杖就搞了個Billbroad粒子糊弄事。


一眼就能看出來簡單實現方法,卻不得不說效果還成。
但是一旦進入深海,大陸的資源被釋放掉後,就會換成一個正規的海浪模擬。


沒有用尖波(Gerstner),海浪也不大,應該就是兩個不同頻率的正弦波的疊加,在頂點上直接計算。
海浪模擬為了節約計算成本,按說會做LOD,不過移動攝像機後發現LOD層級是一樣的,只是到最遠處變回了最初的Quad海面。
為了方便視域裁剪按說要對海面分塊,規則是近處分塊密,遠處分塊稀疏,這樣才能在滿足裁剪的情況下減少DrawCall。

不過它LOD都沒做,估計就是最大粒度按扇形隨便分了下下吧。
另外這遊戲整個海面是跟著主角一起移動的(意外地破綻不大),不需要動態建立銷燬,其實很容易做分塊。
進入深海後海面也有了紋理

兩次取樣,第二次UV翻倍,而且染成黑色紋理。同時還要做一次隨機擾波增加波紋細節。

越遠紋理則越淡。
(五)船隻尾跡
其實就是個隨時間變寬的MeshTail,但是帶了一個偏移每一段UV的正弦擺動效果(兩個不同頻率的正弦波疊加)


貼圖:

作者:flashyiyi
專欄地址:https://zhuanlan.zhihu.com/p/34960962
相關文章
- 達人分享 | 賽璐璐風格與《塞爾達傳說》
- 創作類《塞爾達傳說:曠野之息》風格的水著色器
- 林克整容史:歷代「塞爾達」藝術風格變化回顧
- SVG之Path路徑詳解(二),全面解析貝塞爾曲線SVG
- 說說《塞爾達》服化道那些事
- 媒體一致好評,這部沒有塞爾達的《塞爾達傳說》新作究竟好在哪兒?
- 《塞爾達傳說:荒野之息》是如何做新手引導的?
- 為什麼全世界都在期待《塞爾達傳說:王國之淚》?
- 《塞爾達傳說:曠野之息》:何止優秀,意義深遠
- 《塞爾達傳說:荒野之息》開放世界探究(下):互動機制
- 從App Store到Switch:這個“塞爾達”風的獨立遊戲系列靠什麼成功?APP遊戲
- 《塞爾達傳說·荒野之息》中所隱含的“快樂遊戲”的奧祕遊戲
- 為什麼你不能錯過《塞爾達傳說:荒野之息》的開放世界
- 《塞爾達傳說:荒野之息》的大世界地圖是怎麼製作的?地圖
- 《塞爾達傳說》是怎麼做開放世界的?
- 塞爾達織夢島是如何設計迷宮的
- 從《縮小帽》分析塞爾達系列的關卡結構
- 貝塞爾曲線
- 《塞爾達傳說:曠野之息》技術分析:神作是怎麼煉成的
- E3 2019 | 任天堂直面會彙總 《塞爾達傳說:荒野之息》續作!
- 《原神》的原罪,或許不只是“抄襲塞爾達”
- 致敬血源、塞爾達,黑暗幻想遊戲《狩夜人》發售遊戲
- 當“思嵐”鐳射雷達邂逅盲人柺杖
- 本月玩什麼 | 塞爾達御天之劍 HD、上行戰場、迷你公路、心跳文學部
- 為什麼這麼好玩?領略《塞爾達傳說:曠野之息》精妙設計(上)
- 《塞爾達》關卡發展史:箱庭設計大師課
- Flutter 自定義元件之貝塞爾曲線畫波浪球Flutter元件
- 《塞爾達傳說:荒野之息》隨感:一個活的世界,以及它是如何被殺死的
- 《塞爾達傳說:織夢島》做了場跨越20年的美夢
- 《塞爾達無雙:災厄啟示錄》全球出貨超300萬
- 貝塞爾曲線基礎部分
- 遊戲機制設計:《塞爾達傳說:荒野之息》中的湧現與弱引導設計遊戲
- 《塞爾達傳說:曠野之息》等4款Switch遊戲中國版權申請獲准許遊戲
- 以《塞爾達傳說:曠野之息》和《異度之刃》為例談好點子的重要性
- 《塞爾達無雙 災厄啟示錄》專訪 與《曠野之息》連結的無雙體驗
- Android日常學習:OpenGL 實踐之貝塞爾曲線繪製Android
- HowLongToBeat: 《塞爾達傳說:王國之淚》榮登2023年最耗時遊戲榜首 遊玩時長高達110小時遊戲
- 這家工作室花三年把塞爾達做進RogueLike裡面