對於本屆 BOOOM,我本來覺得我其實沒什麼特別的經驗值得分享,直到今天我發現了這個:
“探路者”這三個字一下子就戳中了我的心巴,好想得到這個徽章啊臥槽。話說這個能做實體的嗎?
好吧,這個徽章我實在是太愛了,為了得到這個徽章,我決定分享一下《寄生絨毛》這個作品中用到的絨毛系統是如何製作的。
說是技術分享,其實就是把我平時的“開發日誌”拿出來給大家看看,而且由於這個所謂的日誌本來是我自己用來做總結記錄的文字筆記,所以大部分內容看起來是不成系統不成體系的,有些甚至前言不搭後語(就我自己能看懂)。在這種情況下,我只能摘取其中比較能看的部分出來給有興趣的朋友做參考。
還要說明的是,這段技術分享中的內容是《寄生絨毛》這個作品比較早期時的實驗結果,後期做了很多調整和優化,這裡只提供最基本的思路,同時,《寄生絨毛》中使用到的絨毛技術要比這段技術分享中所闡述的技術內容更加複雜,模塊也更多。這段技術分享只為拋磚引玉(還有我想要的徽章),如果你也想做這麼一個絨毛系統,還需付出更多努力。
正文:
1. compute shader 離線構造絨毛網格
絨毛網格的構造依賴 compute shader 的 gpu 計算,具體分為兩個步驟:曲面細分(Tess),絨毛分節(Tri)
FK 項目中曲面細分這一步驟的真正實現方法其實可以被叫做暴力細分或者手工細分,是提前規劃好每個三角形的細分方法,然後在每個三角形內創建規劃好的頂點,並創建頂點索引形成新三角形。
上圖中,左側為原始網格,右側為 Tess2 方法細分後的網格
絨毛分節指的是在每個三角形內最少創建一個新的頂點和三個新的三角形,完成後的網格只要拉高這個新創建的頂點就能構造一個三稜錐,我們用這個三稜錐來模擬一根絨毛的形態。為了使絨毛看起來更自然,就勢必要使絨毛能夠隨著外力而表現出抖動效果。為了使抖動效果更自然,就要將每根絨毛分為更多的段,段數越多,絨毛抖動的效果模擬的就越自然。
圖中為被分為兩節的三稜錐,用以模擬絨毛效果
上圖左側為原始網格,右側為 Tri2 方法形成的被分為兩節的三稜錐
製作絨毛網格時,先用 Tess 做細分,再用 Tri 做絨毛分節,這會產生大量頂點。這時就會遇到網格頂點數限制問題。這個問題體現在兩個方面,第一個方面是我們在使用 Tess 和 Tri 方法後,產生的大量頂點因為網格頂點數限制而無法成功被保存為新的網格。第二個問題是即使我們使用一個網格來保存和展示擁有大量頂點的絨毛,那麼後面要做絨毛剪切效果時,每次檢測都要檢測所有頂點,而不能簡單的只檢測固定位置周邊的少數頂點,除非引入更復雜頂點裁剪再做檢測。
為了解決這兩個問題,這裡的做法是將一個球分割成多個塊。然後為每個塊做 TessTri 操作,再將塊拼接成新的球。這樣每個小塊都擁有大量絨毛,效果看起來會比較逼真。不過這個做法也要引入新的網格製作流程。
首先在 blender 中製作球體網格我們面臨著兩個選擇:uv sphere 和 quad sphere。
左:uv sphere 右:quad sphere
從動圖中可以看到兩種 sphere 在上述製作絨毛的流程中產生的兩種絨毛的效果區別。uv sphere 產生的絨毛效果主要問題來自它 uv 展開方式和它所攜帶的三角形的分佈。
uv sphere 做 uv 展開非常容易和規整:
針對FK 項目應用場景 uv sphere 有兩個缺點:
1. 首先是這種做法網格被分割的塊大小差別比較大,這就導致我們上述製作絨毛網格流程會產生密度差別較大絨毛塊。
2. 其次就是 uv sphere 產生的絨毛在視覺上顯得非常不均勻,絨毛的形態和分佈具有規律性(一圈一圈的)
3. 最後就是貼圖不連續導致的撕裂問題。貼圖撕裂會影響所有依賴貼圖的效果,比如我們只用噪點貼圖模擬風吹動絨毛的效果時,由於噪點貼圖也會被撕裂,所以我們可以觀察到風作用在絨毛上的效果也會有很明顯的撕裂。
與 uv sphere 相比 quad sphere 與 FK 項目的適配性更強。下面是 quad sphere 在絨毛生成流程中的製作要點:
1.在對 quad sphere 做 uv 展開時,要以半球為單位展開,並且保證兩個半球的 uv 在最外圈的部分取值相同。實現方式是上下翻轉 uv 並且對齊貼圖:
2.如上述絨毛製作流程中所述,我們要將球體分為很多小塊,在 blender 對球體做分割後,每個小塊的邊緣頂點法線也會產生偏移,如果不做處理,那麼製作出來的絨毛在長度不為 0 時,就會因為生長方向產生問題:
右側圖中可以看到沒有對齊法線導致的空洞
對齊法線的做法是,在 blender 中分割球體前,保留原始網格,原始網格保有原始法線。在分割後先將所有子塊設置 auto smooth,然後為每個子塊添加 Normal Editor 修改器,target 選擇原始網格,這樣就可以將原始網格的法線複製到對應的子塊中。
右側為法線對齊後的示意圖
2. vertex shader 模擬絨毛
FK 項目中絨毛的模擬是使用 vertex shader 來實現的,同時實現的效果依賴於 compute shader 製作絨毛網格時,計算並存儲在頂點法線和 uv 中的值,具體規則如下:
1.絨毛(三稜錐)的最高點頂點(後稱 top 頂點)法線為所在三角形頂點的法線的平均值
2.絨毛的節點的所有頂點(後稱節點頂點)的法線為所屬絨毛的 top 頂點法線,這樣能保證在 vs 中做生長操作時,構成一根絨毛的所有頂點(一個 top 頂點 + 多個節點頂點)都有相同的生長方向
3.保證了絨毛生長方向後,還要保證絨毛節點頂點的生長長度和節點頂點所處位置保持一致。例如一根絨毛有兩個節點,如果生長長度為 1,那麼最下面的節點頂點要生長 1 * 0.33,以此類推。有兩種做法可以達成這個目的,一是節點頂點法線(後稱節點法線)在存儲時,要乘以與節點位置相應的係數,二是單獨將這個係數保存在 uv 中。由於這個係數還要在其他方法中使用,所以為了方便,當前實現是直接保存在 uv2.y 中。
4. compute shader 製作絨毛網格時並不知道絨毛生長的全部信息(除了方向和節點頂點係數),FK 項目目前也沒有將絨毛模擬和生成統一到 compute shader 中,所以如 3 所述節點法線信息存儲的並不是真實的法線,而是生長方向。這就要求我們要在 vs 中重新計算節點法線來保證渲染正確。為了計算節點法線,我們需要在生成絨毛的 Tri 階段中,保存與節點相連的絨毛的根頂點的位置,這個值存儲在 uv1 + uv2.x 中。
5. 節點和 top 頂點的 uv3 存儲了當前絨毛在所有絨毛中的索引值,在絨毛的剪切效果實現中,要剪切的絨毛位置會被 compute shader 計算並存儲在 RT 中,存儲時用來標記存儲位置的 uv 就是這個 uv3 中的值。vs 每個頂點都使用 uv3 讀取這個 RT,讀取到的值,會限制該頂點的生長長度,從而達到絨毛被剪切了一部分長度的效果。
3. 絨毛的裁剪效果
首先,一個絨毛是否被裁剪,取決於該絨毛的 top 頂點是否在裁剪的範圍內,和其他頂點無關。其次,哪些絨毛會被裁剪可以通過動態繪製紋理的方式來記錄。
結合上述兩點我們得知,動態紋理要填充的值是 top 頂點被裁剪的程度值,top 頂點在動態紋理中什麼位置存儲這個值,取決於 top 頂點的索引。
這個索引如何計算?
使用 Tess4 + Tri3 生成的每個子塊大概包含 4000 個 top 頂點,一個完成的球包含 24 個這種程度的子塊。也就是說全部 top 頂點數量不會超過 10萬。一個大小為 512 * 512 的紋理包含 26萬+ 像素,完全滿足每個像素用來存儲一個 top 頂點的索引值的需求。在 compute shader 生成每個子塊時,都傳入當前已經生產的所有子塊所包含的 top 頂點總數,然後 top 頂點在這個總數上計算一個自己的二維索引值(相當找到自己在一個 512 * 512 表格中的位置),並將其存儲在 uv3 中。當前 top 頂點相關的節點頂點保持和這個 top 頂點一致的 uv3 值,vs 中使用 uv3 讀取這個動態紋理中的值,top 頂點和節點頂點就能計算出自己被裁剪的值是多少。
在上述這個過程中,紋理存儲的值是一個用來計算的確定值,所以就必須要保證這個紋理被讀取時,所要讀取的值就是我們計算並存儲的那個值。那麼紋理中存儲的值在被讀取時,會因為哪些影響而變化?
1.sRGB
2.mipmap
3.各向異性
4.紋理過濾,過濾器要選擇 point,否者讀取的值會被過濾器再計算,例如和臨近像素進行插值等
5.存儲時,計算出的 uv 值會被紋理大小和 float 精度影響。512 * 512 大小的紋理,1 紋素 uv = 1 / 512 = 0.001953125。滿足 float 精度,如果是 1024 大小紋理計算 uv 就可能會有問題了
在 FK 實現中,這個動態裁剪位置標記紋理還要用來決定被裁剪的位置的顏色,在 fs 中採樣這個紋理時要使用非 point 過濾器,這樣才能保證顏色值正確,因為 fs 中的 uv 值是被插值後的,使用 point 過濾器會導致最終顏色來回閃爍。並且,出於同樣的原因,動態裁剪位置標記紋理要使用 1024 * 1024,否則其他絨毛渲染顏色時,也會被標記裁剪(雖然不影響長度):
圖中可以看出,除了車子前進軌跡外,其他部分絨毛也有變色