在shader中實現(xiàn)五種描邊方法
輪廓線渲染方法一覽
在RTR3中,作者分成了5種類型(這在《Unity Shader入門精要》的P289頁有講):
基于觀察角度和表面法線 通過視角方向和表面法線點乘結果來得到輪廓線信息。 簡單快速,但局限性大。
過程式幾何輪廓線渲染。 核心是兩個Pass:第一個Pass只渲染背面并且讓輪廓可見(比如通過頂點外擴);第二個Pass正常渲染正面。 快速有效,適應于大多數(shù)表面平滑的模型,但不適合立方體等平整模型。
基于圖像處理。 可以適用于任何種類的模型。但是一些深度和法線變化很小的輪廓無法檢測出來,如桌子上一張紙。
基于輪廓邊檢測。 檢查這條邊相鄰的兩個三角面片是否滿足:(n0·v > 0) ≠ (n1·v > 0)。這里n0和n1分別表示兩個相鄰三角面片的法向,v是從視角到該邊上任意頂點的方向。本質是檢查相鄰兩個三角是否一個面向視角,另一個背向視角。 可以控制輪廓線的風格渲染。缺點是輪廓是逐幀單獨提取的,幀與幀之間會出現(xiàn)跳躍性。
混合上述方法。 例如,首先找到輪廓線,把模型和輪廓邊渲染到紋理中,再使用圖像處理識別輪廓線,并在圖像空間進行風格化渲染。
這里我分別使用?基于觀察角度和表面法線、模板測試、過程式幾何輪廓線、基于圖像處理(屏幕后處理)?的方法對一個簡單場景做了實現(xiàn)。最后簡單使用了?SDF?的方法去進行描邊實現(xiàn)。 由于關注的是輪廓線的渲染方法,故這里我盡量采用最小實現(xiàn),也就沒有考慮光照的效果了。


1.基于觀察角度和表面法線
原理就是之前說的那樣,這里直接放效果和完整代碼,使用一個參數(shù) _Outline 去控制輪廓線的粗細。
實現(xiàn)效果:

2.模板測試描邊
實現(xiàn)原理:


我們這里在第一個pass中正常渲染,把每個片元的參考值 Ref 都設置為1,Comp Always 總是通過模板測試, 并且 Pass Replace (不寫的話默認是 Pass Keep),即把當前的 Ref 寫入模板緩沖。
第二個Pass中,我們把每個頂點按法線方向去進行一個擴張。這里我們選擇先把頂點和法線變換到視角空間下,是為了讓描邊可以在觀察空間達到最好的效果。隨后設置法線的z分量,對其歸一化后再將頂點沿其方向擴張,得到擴張后的頂點坐標。對法線的處理是為了盡可能避免背面擴張后的頂點擋住正面的面片。最后,我們把頂點從視角空間變換到裁剪空間。
在這個Pass中,我們同樣把每個片元的參考值 Ref 都設置為1,Comp NotEqual 即只有當前參考值 Ref 和當前模板緩沖區(qū)的值不相等的時候才去渲染片元。注意到,Unity的模板緩沖區(qū)的默認值是0,因此在外輪廓線之內的片元,我們在第一個Pass中寫入到模板緩沖區(qū)的值為1,因此第二次Pass中相等,就不會去選擇渲染;而外輪廓線向外擴張出來的頂點所形成的那些片元,由于第一個Pass并未渲染,模板緩沖區(qū)的值為0,因此不相等,就會按第二個Pass的方法得到結果。

3.過程式幾何輪廓線渲染
實現(xiàn)原理:
其實就是把前面的模板測試換成了剔除操作。正常渲染的時候剔除背面渲染正面,第二次頂點擴張之后剔除正面渲染背面,這樣渲染背面時由于頂點外擴的那一部分就將被我們所看見,而原來的部分則由于是背面且不透明所以不會被看見,形成輪廓線渲染原理。因此從原理上也能看出,這里得到的輪廓線不單單是外輪廓線。

4.邊緣檢測
這種方法其實是用屏幕后處理效果去實現(xiàn)的(也就是基于圖像處理)。
屏幕后處理,通常指的是在渲染完整個場景得到屏幕圖像后再對這個圖像進行一系列操作實現(xiàn)各種特效。這里實現(xiàn)的原理其實是使用特定的材質去渲染一個可以剛好填充整個屏幕的四邊形面片。
而邊緣檢測的原理其實就是用一個特定的卷積核去對一張圖像卷積,得到梯度值,再根據(jù)梯度值的大小去判斷是否為邊界。


具體代碼可以看《Unity Shader入門精要》源碼,場景為Scene_12_3:
https://github.com/candycat1992/Unity_Shaders_Book
5.SDF方法
關于SDF我在之前的文章中有過分析:
https://zhuanlan.zhihu.com/p/398656596
之前也在UE4中實現(xiàn)過,但是還是剛接觸Unity Shader沒幾天,對shaderlab還不熟悉。這里主要參考了前輩的文章,在其基礎上稍作修改:
https://zhuanlan.zhihu.com/p/26217154

原理其實很簡單,這里的圓是在shader中根據(jù)SDF值繪制的。SDF值在邊界處接近0,于是我們就通過SDF的fwidth值與當前像素的SDF值去判斷,因為fwidth為相鄰像素的SDF差值和,那么必然很小。所以判斷的結果用于lerp,就可以檢測哪里的SDF值接近0,亦即檢測到輪廓。而aa也是簡單地用smoothstep處理就好。
關于基于輪廓邊檢測的方法
再來回顧一下之前所述的原理:
檢查這條邊相鄰的兩個三角面片是否滿足:(n0·v > 0) ≠ (n1·v > 0)。這里n0和n1分別表示兩個相鄰三角面片的法向,v是從視角到該邊上任意頂點的方向。本質是檢查相鄰兩個三角是否一個面向視角,另一個背向視角。
于是這里我想到用幾何著色器去做,但是不知道怎么獲得相鄰的三角面片,在OpenGL中有 GL_LINES_ADJACENCY 去得到線段以及相鄰頂點,就正好四個頂點兩個相鄰面片,從而可以去處理。但是Unity Shader中我沒有找到怎么做。但是在谷歌中搜索出了一個解決方法:
https://forum.unity.com/threads/does-unity-support-triangleadj-in-geometry-shaders.930306/
https://github.com/Milun/unity-solidwire-shader/blob/master/Assets/Shaders/SolidWire.shader
關于可選頂點著色器
之前我整理了渲染管線:
https://zhuanlan.zhihu.com/p/408238134
但是關于曲面細分著色器和幾何著色器沒有詳細說明,這里補充一下:

如上圖,曲面細分又分為:Hull shader 、Tessellation Primitive Generator 、 Domain shader,這些名稱在不同的API中可能不一樣。
對于曲面細分著色器,輸入是Patch,可以看成是多個頂點的集合,包含每個頂點的屬性。功能是可以將圖元細分。輸出為細分后的頂點。
對于幾何著色器,輸入為渲染圖元,輸出則為一個或者多個圖元,同時還要定義輸出的最大頂點數(shù),并且輸出的圖元需要自己構建(順序很重要)。在知乎上找到一個最簡單的入門:https://zhuanlan.zhihu.com/p/141036227
https://www.bilibili.com/video/BV1XX4y1A7Ns/?p=2
引用: https://zhuanlan.zhihu.com/p/410710318