數(shù)字人技術(shù)現(xiàn)在已經(jīng)是一項(xiàng)比較完善的技術(shù),,通過Unity引擎的烘焙與深度渲染,現(xiàn)在的數(shù)字人已經(jīng)變得相當(dāng)逼真,。
在本文中來自完美世界移動(dòng)項(xiàng)目支持部的徐行跟大家分享了他在完美世界研發(fā)的 Unity 毛發(fā)系統(tǒng),。
“其實(shí)這個(gè)作品還在很初期的階段,還有很大的提升空間,,這次主要是讓她幫忙展示一下毛發(fā),。這個(gè)是本工作的大概時(shí)間線,其實(shí)前期還不配被稱之為自研毛發(fā)系統(tǒng),,只能稱得上是一個(gè)英偉達(dá) HairWorks SDK 的集成,。隨著后期需求越來越深入,,自研的部分也越來越多?,F(xiàn)在只是思想上部分參考了 HairWorks,代碼已經(jīng)全部重寫了,?!毙煨姓f道。
現(xiàn)在游戲中比較傳統(tǒng)的毛發(fā)解決方案有兩類,,第一類就是網(wǎng)格頭發(fā),,這類頭發(fā)其實(shí)渲染效果還不錯(cuò),,但是往往物理效果不太好,而且也不太適合做短毛,。
Uncharted by NaughtyDog
另一類是 FurShell,,專門用來做短毛的。但是湊近了看,,層狀瑕疵也非常明顯,。做毛發(fā)的終極解決方案,肯定就是基于發(fā)絲的毛發(fā)系統(tǒng),。這是一些有代表性的案例,,比如《最終幻想》、《怪獸公司》,、《長(zhǎng)發(fā)奇緣》,、《古墓麗影》和《巫師 3》。
雖然基于發(fā)絲的方案效果很棒,,但是問題也是顯而易見的,。那就是發(fā)絲數(shù)量太多,導(dǎo)致無論是物理模擬還是渲染還是存儲(chǔ)等等都有比較大的困難,。
HairWorks 是如何應(yīng)對(duì)這些挑戰(zhàn)的呢,?這就得從它的資源表示方式講起了。HairWorks 的毛發(fā)資源并不是直接存儲(chǔ)每根發(fā)絲的信息,,而是主要存儲(chǔ)了兩個(gè)東西,,圖中的黃線被稱之為導(dǎo)發(fā),圖中的網(wǎng)格被稱為生長(zhǎng)網(wǎng)格,。而物理模擬則在導(dǎo)發(fā)上進(jìn)行,,發(fā)絲則是導(dǎo)發(fā)間插值出來的,這樣就能極大的減少計(jì)算量和存儲(chǔ)間的需求,。
發(fā)絲具體是怎么插值的呢,?其實(shí)生長(zhǎng)網(wǎng)格是由一些三角面構(gòu)成的。生長(zhǎng)網(wǎng)格上面每一個(gè)頂點(diǎn)對(duì)應(yīng)一根導(dǎo)發(fā),,隨機(jī)生成一定數(shù)量的重心坐標(biāo),,在渲染的時(shí)候就可以利用重心坐標(biāo)作為權(quán)重,在三根導(dǎo)發(fā)間進(jìn)行插值了,。
為了讓生成的發(fā)絲更加平滑,,在生成發(fā)絲前還可以對(duì)導(dǎo)發(fā)做一些平滑,使用平滑過的導(dǎo)發(fā)進(jìn)行插值,。
物理模擬又是怎么做的呢,?HairWorks 選擇了一種非常易于理解的物理模擬方法,也就是質(zhì)點(diǎn)彈簧法,。圖中的紅線就是進(jìn)行物理模擬的導(dǎo)發(fā),,紅線上的小圓圈就是質(zhì)點(diǎn),,發(fā)根處的質(zhì)點(diǎn)是完全受骨骼蒙皮控制的,其他的質(zhì)點(diǎn)則會(huì)受到諸如風(fēng)力,、重力等等的影響,,也會(huì)跟碰撞體發(fā)生碰撞。
為了使基于質(zhì)點(diǎn)的物理模擬能夠體現(xiàn)毛發(fā)的感覺,,HairWorks 為質(zhì)點(diǎn)間增加了一系列的約束,,也就是俗稱的彈簧。比如圖左中,,同一個(gè)頭發(fā)上相鄰質(zhì)點(diǎn)間有長(zhǎng)度約束,,太近了會(huì)排斥,太遠(yuǎn)了會(huì)吸引,,這有助于保持頭發(fā)的大致長(zhǎng)度,。
再比如黃色折線,它表示的是完全受骨骼蒙皮控制的毛發(fā),。它跟紅色這條線,,也就是物理模擬控制的毛發(fā)間也有約束,這有助于保持美術(shù)所制作的造型,。
另外,,HairWorks 還有一個(gè)比較有特色的約束,就是在相鄰的導(dǎo)發(fā)之間也有距離約束,,有助于保持毛發(fā)的體積感,,一定程度上避免穿插。當(dāng)然還有很多其他種類的約束我們就不一一展開提了,。
為了加速物理模擬,,HairWorks 的物理模擬是在 Computer Shader 中并行進(jìn)行的。一個(gè)控制點(diǎn),,也就是一個(gè)質(zhì)點(diǎn)對(duì)應(yīng)一個(gè)線程,,一個(gè)導(dǎo)發(fā)對(duì)應(yīng)一個(gè)線程組,這就便于使用效率比較高的共享存儲(chǔ),。
但是有些約束是有先后順序依賴關(guān)系的,,例如同一個(gè)質(zhì)點(diǎn)上的兩個(gè)長(zhǎng)度約束,,如果調(diào)換執(zhí)行順序,,那么執(zhí)行結(jié)果就不一樣了,。正確的做法只能是串行解算這些約束,,但是串行顯然是不如并行快的,。為了加速物理模擬,,HairWorks 還是想辦法做到了并行,。
如圖,,它把長(zhǎng)度約束分為兩組,,一組內(nèi)每個(gè)約束都互不相臨,。這樣一組的約束就可以并行解算,一組解算完之后,,再解算另一組,,交替迭代幾次就能獲得比較穩(wěn)定的結(jié)果。
“當(dāng)然后面我們還做了一些物理模擬的拓展和優(yōu)化,,例如加入了 3D 風(fēng)場(chǎng)以及允許傳入不穩(wěn)定的物理模擬 TimeStep 等等,。”徐行說道,。
“接下是關(guān)于引擎與跨平臺(tái)的一些分享,,這張圖是我們把 HairWorks 集成到自研引擎之后,在《笑傲江湖》和一些 Demo 中的效果,。當(dāng)時(shí)另一個(gè)端游項(xiàng)目看到了,,覺得效果不錯(cuò),挺想用,。他們是基于 Unity 開發(fā)的,,所以接下來我就開始了向 Unity 的集成?!毙煨姓f,。
其實(shí)遠(yuǎn)在完美世界之前,已經(jīng)有很多團(tuán)隊(duì)進(jìn)行了這項(xiàng)工作,。比如圖中是 Unity Japan 團(tuán)隊(duì)的成果,,但是他們做的集成時(shí)間都比較早了,當(dāng)時(shí)條件有限,,所以也存在一些問題,。比如無法使用 Unity 內(nèi)部的材質(zhì),或者光照不全,,沒有平行光的投影等等,。
他們?yōu)槭裁磿?huì)遇到問題呢?這就要講到他們集成的實(shí)現(xiàn)原理,。
他們選擇了原生插件這種集成方式,,是由于 HairWorks 要用到諸如 Computer、Tessellation 這類很底層的圖形功能,。所以說使用能達(dá)到底層圖形設(shè)備接口,,例如 D3D Device 的原生插件機(jī)制還是比較穩(wěn)妥的。
但是也正是因?yàn)樗麄兪鞘褂玫讓訄D形設(shè)備接口,,例如 D3D Device,,進(jìn)行渲染,而不是使用 Unity 提供的接口例如 DrawRenderer,所以 Unity 內(nèi)的材質(zhì),、燈光,、環(huán)境以及渲染管線內(nèi)部的很多信息,都很難傳過去,,也就很難在原生插件里面重現(xiàn) Unity 的渲染效果,,所以原生插件渲染出來的東西往往光照不全。
另外由于 Built-In 管線中提供的插入點(diǎn)有限,,所以沒有辦法渲染陰影深度,。上述問題如果按原有思路做下去,是很難解決的,。但是這些問題不解決又沒有辦法實(shí)際投產(chǎn),,所以這些集成最后基本上就被擱置了。
徐行的團(tuán)隊(duì)最初也是一籌莫展的,,差點(diǎn)因此放棄,,但是最后還是想到了一個(gè)解決方案,那就是既然 Unity 里面的東西很難拿出來,,是不是可以不把他們拿出來放到原生插件里面,?而是想辦法把原生插件生成的 HairWorks 的幾何信息傳到 Unity 里面,這樣就可以在 Unity 里做光照了,,這樣可行嗎,?
徐行的辦法其實(shí)很簡(jiǎn)單,他使用了一種名為渲染代理的方法,。所謂的渲染代理其實(shí)就是一個(gè)在 Unity 里面創(chuàng)建的頭發(fā)的包圍體,,原生插件把 HairWorks 的幾何信息渲染到了自己創(chuàng)建的 GBuffer 中。
然后渲染代理掛上了 Unity 內(nèi)的材質(zhì),,直接參與 Unity 的渲染,。與普通材質(zhì)有所不同的僅僅是在渲染的時(shí)候會(huì)讀取我的 GBuffer 中的幾何信息,并把它偽裝成自己的進(jìn)行光照,。這樣的話,,就可以直接利用 Unity 自帶的渲染機(jī)制,所以渲染出來的東西跟 Unity 是可以完美融合的,。
這個(gè)機(jī)制還有另外的幾個(gè)好處,,第一是對(duì)項(xiàng)目的渲染管線沒有影響,可以直接使用 Unity 材質(zhì),,也支持 Shader Graph,。所以不管對(duì)美術(shù)用戶還是技術(shù)用戶都比較友好。
另外,,Unity 很多時(shí)候需要把一個(gè)物體渲染多次,,而使用渲染代理的話,,就可以避免多次提交復(fù)雜的毛發(fā)幾何體去渲染了。因?yàn)槲颐看翁峤坏?,都只是一個(gè)很簡(jiǎn)單的包圍體,。
最后一個(gè)優(yōu)勢(shì),大家可以看出來這個(gè)機(jī)制很類似于延遲渲染,,每個(gè)像素上只需要進(jìn)行一次著色,,所以是比較高效的,。渲染可以使用代理,,投影也可以使用代理,如此一來集成就變得很簡(jiǎn)單了,,就不存在之前所說的諸多問題了,。當(dāng)然,后來有了 SRP,,就可以在渲影子的時(shí)候直接調(diào)用原生插件渲染了,,不再需要投影代理了。
接下來就是跨平臺(tái),,其實(shí)剛剛不管是 Unity Japan,,還是集成,一直是通過原生插件對(duì)接 Nvidia HairWorks SDK 做的,。在 SRP 誕生之前,,這可能是唯一能夠跑通的方法。HairWorks SDK 預(yù)留了跨平臺(tái)的設(shè)計(jì),,但是 Nvidia 只實(shí)現(xiàn)的 DX11 和 DX12 的版本,,剩下所有的圖形接口都需要去重新實(shí)現(xiàn)一份 HairWorks SDK 的底層,這個(gè)工程量是比較大的,。而且由于很底層,,所以難度也比較高。徐行團(tuán)隊(duì)花幾個(gè)月的時(shí)間才實(shí)現(xiàn)了一次 PS4 平臺(tái),,雖然是做完了,,但是做完之后覺得不能再用這種方式繼續(xù)往下做了。
得益于近些年 Unity 的進(jìn)化,,特別是 SRP 的加入,,讓直接在 Unity 內(nèi)實(shí)現(xiàn)這種復(fù)雜功能變成了可能。我利用近期 Unity 提供的一些新功能,,例如 Computer Shader,、CommandBuffer、RenderFeature,、CustomPass 等等,,直接把紅框內(nèi)的整個(gè)流程包括資源加載、約束初始化、物理模擬,、幾何體渲染,,這些全部都在 Unity 內(nèi)部重新實(shí)現(xiàn)了一遍。
這個(gè)流程比之前的 PS4 移植要順利得多,,因?yàn)楝F(xiàn)在 Unity 的渲染開發(fā)是很高效的,,做任何修改都不需要關(guān)編輯器,也不需要花很長(zhǎng)的時(shí)間來編譯,,可以即時(shí)看到效果,。
另外由于有像 CustomPass 和 RenderFeature 這樣方便的機(jī)制,對(duì) SRP 的源碼修改其實(shí)只有幾行,。
接下來講一下毛發(fā)著色,,我們可以先看一下最終的效果。
說到毛發(fā)著色,,我們首先會(huì)想到的就是 Kajiya-Kay 模型,,它用毛發(fā)的切線替代了常用的法線,把 cos 換成了 sin,,快速實(shí)現(xiàn)了類似于頭發(fā)的效果,。
但是如圖所示,雖然它產(chǎn)生了類似于頭發(fā)的高光,,但是塑料感很強(qiáng),。在 2003 年 Marschner 提出了一種更接近物理真實(shí)的毛發(fā)著色模型,中間就是 Marschner 成果,,右邊是真人照片,。在不考慮造型的情況下,可以說效果已經(jīng)很接近真實(shí)的照片了,。
它首先是對(duì)與毛發(fā)產(chǎn)生的交互光線進(jìn)行分類,,打在毛發(fā)表面就被反射的光線被稱為 R。打入頭發(fā),,然后又從頭發(fā)背面射出的光線被稱為 TT,。打入頭發(fā),在頭發(fā)內(nèi)部被反射,,又從頭發(fā)正面透出來的光線被稱為 TRT,,這里面 R 和 T 分別代表反射和透射。
光線在頭發(fā)內(nèi)部行進(jìn)的過程中,,他的部分能量會(huì)被頭發(fā)的內(nèi)核吸收,,所以說圖中的 TT 和 TRT 都會(huì)帶有頭發(fā)的顏色,而R就類似于一般的高光,,不受頭發(fā)顏色的影響,。
另外,,由于頭發(fā)表面的鱗片與毛發(fā)切線形成了一定的角度,這導(dǎo)致出射的 R 和 TRT 的角度產(chǎn)生了一定的偏差,。直觀地來說,,就是他們兩個(gè)的中心是分離的。
如中間的部分所示,,白色的高光是 R,,最上面呈現(xiàn)發(fā)色的高光就是 TRT,可以看出他們的中心是有一定偏差的,。
Marschner 的另一項(xiàng)重要貢獻(xiàn),,就是將散射光線在毛發(fā)橫截面和縱截面的能量分布分開建模,這極大的簡(jiǎn)化了建模的難度,,減少了單個(gè)分布函數(shù)的參數(shù),。
盡管如此,,這個(gè)模型的數(shù)學(xué)計(jì)算還是十分復(fù)雜的,。在 Shader 實(shí)現(xiàn)一套比較麻煩,性能也比較低,。
為了提高計(jì)算性能,,英偉達(dá)很早就提出了一種簡(jiǎn)便的方法,就是把橫截面和縱截面出射光的能量分布烘培到兩張紋理里面,,渲染的時(shí)候查詢即可,。這種方法又被稱之為查找表法。圖中就是英偉達(dá)用這種技術(shù)制作的美人魚 Demo,。
但是這個(gè)方法有一個(gè)的問題,,就是只支持一種頭發(fā)顏色和一種粗糙度。
受到英偉達(dá)思路的啟發(fā),,徐行寫了一個(gè)很簡(jiǎn)單的光追小程序,,從各個(gè)方向向毛發(fā)發(fā)射光線,再在各個(gè)方向統(tǒng)計(jì)出射光線的能量分布,,最后把能量分布存入紋理中,。
但是在這個(gè)思路的基礎(chǔ)上,他又做了兩個(gè)擴(kuò)展,。第一是徐行希望美術(shù)能夠調(diào)節(jié)粗糙度,,所以他將 32 個(gè)不同粗糙度的能量分布都烘焙了,排成了一個(gè)序列,。
第二是在能量分布圖中,,沒有包含毛發(fā)中心對(duì)光線吸收,而是用了一組新的紋理來記錄光線在毛發(fā)中行進(jìn)的平均距離,。根據(jù)這個(gè)距離和美術(shù)設(shè)定的毛發(fā)顏色換算得到的吸收率,,就可以對(duì)毛發(fā)光線能量進(jìn)行衰減,,從而體現(xiàn)不同的毛發(fā)顏色。
最終的效果如圖,,大家可以從圖左中觀察到 R 和 TRT,,可以在圖右中觀察到明顯的 TT。