2019年11月17日 星期日

[Unity] 快速刷出邊緣光

●圖1. 一台車加上刷光特效

首先感謝 https://free3d.com/3d-models/vehicles
這個網站,上面真的有蠻多素材可以測試
找素材的時間我都寫完10份演算法了XD

目標

簡單來說,目的就是丟一個模型進去,計算完邊緣之後將結果存下來。
這種邊緣的計算跟抗鋸齒要的不太一樣,抗鋸齒可以即時處理,但是想要這種刷光是需要前置輔助的。

如果不自動計算,美術就只能一碗滷肉飯一瓶礦泉水一筆一畫畫在想要的位置上,實在很沒效率。



●圖2. 這個要美術畫大概會起笑吧

演算法

演算法很單純,邊緣查找,不外乎就是把所有頂點一個一個打斷鼻樑抓進來,然後兩兩比較他們的法線的內積


這個公式大家再熟悉不過了,當a跟b都是單位向量的時候角度與內積值會呈反比。

所以工作相當的單純:
  1. 先給一個頂點的距離門檻值,篩掉距離太遠的點 (邊緣之所以叫邊緣就是他們很接近)
  2. 計算兩兩點內積值,若有多重個點,加總內積值以及次數
  3. 平均內積值,輸出到貼圖上面 (想加粗可以改成min,想變細可以改抓max,看需求)

實作方式

從邊緣計算的方式我們可以知道,點數越大的模型,出來的效果越好
因為如果面數太低,點與點之間的距離必定會加大,變成距離門檻值要拉很高才算得出東西,這樣不論怎麼算都會是粗線條。

因此,範例我抓了一個96000點的模型來使用,不過問題來了,這個計算量會是相當龐大的。
96000*95999 =  9215904000
92億次的計算????市面上絕對沒有CPU撐得住

如果修改演算法,先根據距離門檻值把點做分群再算呢?
可能值得一試,但先分群反而還多了前處理的時間,根據切法不同有時候可能還不會真正省到時間

所以,維持簡單的算法,丟到GPU上面算吧!
  • 方法一、 Geometry Shader

    Geometry Shader允許我們輸入一個三角形,一次針對一整個三角形來處理,其中更有一種輸入型別 triangleadj,可以抓取鄰居三角形! (距離門檻值都省了)


    illustration of the various primitive types for a geometry shader object 

    本來的input是0 1 2,丟入triangleadj就會變成去處理024 012 045 234
    這樣邊緣資訊就唾手可得了,不過,這個方法卻有個致命的缺點....
    就是Unity不支援,掰掰洗洗睡


  • 方法二、Compute Shader

    另一個方法,自然就是用Compute Shader加速了!它的特性是多重thread平行化計算,對於這種大量但是規則單一的動作是最為拿手的了!








    在計算時,我的頂點以16383一組為單位,去呼叫compute shader,一次使用1024個threadgroup處理,一個threadgroup又有1024個threads在執行,所以96000*96000次計算,實際上被我打散成 16 * 94 = 1504次!
    如此一來,計算上就是節省為 96000 / 16383 * 1504次,八千多次即可完成
    這實在是非常多的節省~

    當然要用compute shader只得一個缺點: 需要D3D11
    不過現在應該沒有顯卡不支援,若果一個專案用不起支援D3D11的顯卡就趕快逃吧


     

計算結果的優化

最後結果的輸出,絕對不是直接用模型的uv來寫值到貼圖上,要知道compute shader可不像fragment shader一樣,會自動幫我們把pixel之間做線性內插,如果直接輸出根本無法使用:



因此,這邊的輸出並不是最終輸出,還要再做一次smooth的動作,輸出的方式為:


首先創一張足夠容納模型點數的像素圖 (96000開根號 = 310 * 310大小的圖)
只是將它作為一張查找表來使用 (變成256*256只是我輸出一份來預覽,實際上是310*310)

↑哥存的是查找貼圖,不是渲染貼圖


然後,再把這張查找表丟到另外一個Shader做處理:
↑最重要的部分

首先,我們必須把數值取出來,所以這邊利用SV_VertexID系統關鍵字,來取得vertex id,然後做了跟我剛剛在compute shader做的轉換,利用vertex -> fragment shader會幫我們內插數值的特性來優化結果!

其實這個結果就相當於把數值儲存在model裡面使用,但是我不想在即時渲染的時候還是使用高面數版本的模型,我希望能夠低面數存取貼圖的情況

所以,這一小段第二個重點,就是clip-to-texture space的小轉換了
一般情況下,我們做頂點轉換的時候,最終都會投影到[-1,1]這個稱為NDC (Normalized Device Coordinate) Space的空間,所以這邊就是利用模型的uv,假設uv絕不重疊且位於[0,1]之間,透過簡單的轉換轉成[-1,1]就可以了

如此一來,圖2的結果就會展現出來


總結

輸入一個高面數模型 (假設normal正確,uv不重疊並介於0~1之間)
透過compute shader快速取得邊緣值
再透過mapping-to-texture的方式輸出結果
最後就可以使用低面數的模型來繪製出刷光的結果!
生成出來的圖亦可以再做模糊或其它後製處理來平滑細節