渲染TA實(shí)戰(zhàn):模型草美術(shù)效果分享

Hi!大家好我是小圓,來(lái)自暢游引擎部TA組。這次分享的內(nèi)容是模型草的美術(shù)實(shí)現(xiàn),包括草的模型和shader以及renderer Feature的制作。最后實(shí)現(xiàn)的效果:簡(jiǎn)單的風(fēng)格化的光照效果、草隨風(fēng)搖擺、和草的穩(wěn)定碰撞交互、以及和草帶有彈力的碰撞交互。

↑這是草的Shader面板的一些可調(diào)屬性

↑這是配合Renderer Feature做出的草的碰撞效果,一種更加穩(wěn)定便宜,另外一種富有彈性。
特別提前說(shuō)明說(shuō),本篇分享主要關(guān)注在草的動(dòng)態(tài)效果,包括風(fēng)拂動(dòng)、和角色的交互。草的大規(guī)模生成、存儲(chǔ)、實(shí)例化渲染不在分享的主要關(guān)注范圍內(nèi)(因?yàn)橛型伦隽撕馨舻腉PU驅(qū)動(dòng)的大規(guī)模草地渲染,就不用我來(lái)做了哈哈哈手動(dòng)狗頭)
那我們開始進(jìn)入正文。

從做一簇草的模型開始

做一簇草,首先我們需要幾棵長(zhǎng)短不一的草作為基礎(chǔ)。草的根部因?yàn)榛蝿?dòng)幅度相對(duì)尖端幅度較小,可以分段略寬,在草中段和尖端要分段要做的稍微細(xì)致一些。同時(shí)我們也可以為單獨(dú)的草做好LOD,方便后續(xù)LOD的制作。我這里分了5級(jí)LOD,LOD3和4里,最短的那一根草我直接刪掉了,較小的草在遠(yuǎn)距離下也看不到。這里的草可以略微帶一點(diǎn)弧度,但弧度不要過(guò)大。不然后續(xù)在計(jì)算草的受力效果時(shí)會(huì)看起來(lái)拉伸嚴(yán)重。

有了基本的草面片,我們就可以手動(dòng)隨機(jī)的旋轉(zhuǎn)、縮放我們做好的單棵草的模型,就能獲得一簇看起來(lái)還行的草。擺放我們的草的時(shí)候,需要注意:
高中低三種不同長(zhǎng)度的草在縮放的時(shí)候,不要串了高度順序。尤其低矮的草放太大,分段不夠的時(shí)候會(huì)看起來(lái)比較丑陋
不同的LOD級(jí)別下,想同的一棵草位置一定要對(duì)齊。高級(jí)別的LOD可以適當(dāng)刪掉一些低矮的、細(xì)小的草。
一簇草我們需要確定它生長(zhǎng)在一定的范圍里,比如我這里所有的草都生長(zhǎng)在一片2m x 2m的范圍內(nèi)。這個(gè)數(shù)據(jù)之后的Shader里會(huì)需要使用。
有一些教程里有草的模型的另外一種制作方式是:所有的基礎(chǔ)草面片都直指向天,沒(méi)有弧度。后續(xù)的彎折和傾斜完全靠Shader來(lái)控制。這樣做確實(shí)也可以,但個(gè)人覺得這樣做會(huì)稍微難控制一些,所以直接在一簇草的模型里做一些預(yù)設(shè)的、不太大的彎曲和傾斜。
有了基礎(chǔ)的草的模型之后,我們還需要對(duì)草的UV進(jìn)行一些額外的操作。可能這些操作現(xiàn)在看起來(lái)莫名其妙或者解釋不太直觀,但這對(duì)于后續(xù)的計(jì)算很有必要:
為草添加一個(gè)UVW編輯器,在UV0里(max里顯示貼圖通道1),全選面片、從任意一個(gè)側(cè)面進(jìn)行平面投射。塌陷掉修改器。做好之后的UV0應(yīng)該與下圖類似。

2. 在場(chǎng)景中制作一塊能完全覆蓋所有草根部的面片,這里我的面片大小是2m x 2m,和我之前種草的范圍一致。同時(shí)選中他們并為他們添加UVW編輯器,切換到UV1(max里顯示貼圖通道2),使用平面投射從Z方向(頂視角)進(jìn)行投射。

3. 然后我們需要將每一棵草的每一個(gè)UV點(diǎn),都挪到這一棵根部的UV位置上去。最后實(shí)現(xiàn)的效果應(yīng)該是:每一棵草的所有UV點(diǎn),都在同一個(gè)位置,且這個(gè)位置是草原先根部UV所在的位置。做好后的UV應(yīng)該類似下圖。完事之后就可以塌陷掉這個(gè)修改器,刪掉我們輔助用的面片了。


4. 我們需要對(duì)我們所有LOD進(jìn)行同樣的上述操作.
上邊這一通操作下來(lái),怕是要瞎了不少模型大哥的眼睛,尤其對(duì)齊UV1里每棵草的UV到根部,簡(jiǎn)直不要太煩人。如果能寫個(gè)腳本那真是…誒?好像我已經(jīng)寫了一個(gè)?

好的,至此,我們的草的模型就已經(jīng)制作完畢了。
這里簡(jiǎn)單解釋一下UV0和UV1的用途:
UV0主要記錄了每一棵草的相對(duì)高度,且整體高度被縮放在了0-1之間。這樣我們可以在shader里比較容易獲取到草的尖端和根部。如果要非常嚴(yán)格的計(jì)算草的尖端和根部,也可以對(duì)每一根草進(jìn)行投影、將每一根草的高度縮放到0-1。
UV1里記錄了每一棵草根部的位置,之后我們?cè)赟hader里計(jì)算草所受的力時(shí),需要使用UV1里記錄的數(shù)據(jù)。這樣可以避免同一棵草的不同位置,受到不同方向的力,導(dǎo)致錯(cuò)誤的拉伸。
02-基礎(chǔ)的顏色、和風(fēng)拂動(dòng)的效果
模型之后來(lái)看看Shader的基礎(chǔ)部分。
首先是草的顏色,可以定義三個(gè)Color和三個(gè)Position,然后根據(jù)草的UV0里的y值來(lái)插值。顏色的a值我用做了Smoothness。



然后是法線部分,我們目前沒(méi)有在DCC里對(duì)草的法線進(jìn)行處理,草的法線還是默認(rèn)和表面垂直。這樣的效果對(duì)于寫實(shí)風(fēng)格的草來(lái)說(shuō)或許可行,但對(duì)于風(fēng)格化的草來(lái)說(shuō)就過(guò)于雜亂了。在Vertex階段我們可以將草的法線修改為全部指向天空(float3(0, 1, 0))。

這里扭轉(zhuǎn)法線的操作是在模型空間進(jìn)行的(實(shí)際上我直接傳了float3(0, 1, 0)進(jìn)去,替換了原來(lái)attribute.normal)。法線部分的修改后續(xù)還會(huì)進(jìn)一步調(diào)整,我們暫且先放在這里。

然后我們來(lái)看草的受力部分。在演示的示例中,草受到兩種力:一個(gè)平移的噪聲圖模擬的風(fēng)力、和場(chǎng)景內(nèi)碰撞體產(chǎn)生的推力。為了正確的計(jì)算受力,我們需要先算出一個(gè)用于計(jì)算受力的世界空間坐標(biāo)。記得我們?cè)谥谱髂P偷臅r(shí)候,UV1里存的是每根草的根部的位置信息嗎?我們?cè)陧旤c(diǎn)著色器里先把它還原到模型空間坐標(biāo),再轉(zhuǎn)換到世界空間里去。這樣每一根草的所有頂點(diǎn),在世界空間里將會(huì)擁有同一個(gè)坐標(biāo)。用這個(gè)坐標(biāo)來(lái)計(jì)算受力可以幼小的避免模型的過(guò)渡拉伸。

然后我們就可以用這個(gè)世界空間坐標(biāo)來(lái)采樣一張隨機(jī)的法線噪聲了。演示這里的噪聲是用Substance Designer里的Cloud噪音、用不同種子填充了RGB三個(gè)通道,實(shí)際上我們只會(huì)用兩個(gè)通道,另外的通道是給別的shader用的。

采樣前用內(nèi)置的_Time來(lái)對(duì)UV做平移,然后再對(duì)貼圖進(jìn)行采樣、縮放到-1到1。這時(shí)候我們的風(fēng)力效果還是完全隨機(jī)的狀態(tài),可以給它加上貼圖平移方向的力,來(lái)模擬持續(xù)的、同方向的風(fēng)力。這里要注意貼圖平移方向和風(fēng)力方向是一致的、和UV平移方向相反。

到這里,草的隨風(fēng)搖擺,在世界空間位置相近的位置,受力也非常接近,為了讓每一棵草都有自己的個(gè)性,我們可以用之前計(jì)算出的草的位置,求得一個(gè)隨機(jī)的0-1之間的小數(shù)。乘在我們之前采樣好的平移風(fēng)力圖上。得到風(fēng)吹拂的效果。


↑使用位置隨機(jī)0-1的debug效果。使用位置和一個(gè)向量點(diǎn)積再乘一個(gè)很大的數(shù)后截取小數(shù)部分,一個(gè)經(jīng)典的偽隨機(jī)算法
我們的草在收到風(fēng)力搖擺的時(shí)候,頂點(diǎn)在水平方向移動(dòng)的時(shí)候,也會(huì)在豎直方向上移動(dòng)。如果要準(zhǔn)確的計(jì)算草的彈性形變的話,僅僅在頂點(diǎn)著色器里是很難做到的。這里我們使用一種近似的計(jì)算:計(jì)算每個(gè)草的頂點(diǎn)到垂直于地面的線段,在頂端受橫向力開始圍繞與地面交點(diǎn)旋轉(zhuǎn)時(shí),下降的高度。
然后就可以把風(fēng)力對(duì)頂點(diǎn)的偏移,加進(jìn)頂點(diǎn)的世界空間坐標(biāo)了。這里需要注意,所有的位置偏移在靠近草根部分位置都是逐漸變小的,我們直接把模型空間的y坐標(biāo)乘在偏移的力上。

↑這里的force.xy其實(shí)之前的Wind2D
另外這個(gè)時(shí)候我們可以對(duì)法線做進(jìn)一步的修改了,因?yàn)橛酗L(fēng)力的加入,我們可以將風(fēng)場(chǎng)的力加入到對(duì)法線的影響里。

↑將float3(0,1,0)和草的頂點(diǎn)偏移求加權(quán)平均。1.5是個(gè)經(jīng)驗(yàn)值,可以在GUI上暴露給美術(shù)調(diào)整。

3- 碰撞交互
計(jì)算和物體的交互,核心要點(diǎn)是將物體的碰撞信息傳遞給草的shader。我們案例里的方法是:使用一個(gè)略大于角色(在案例里是個(gè)球)的盤型模型,將它的法線通過(guò)Render Feature渲染在一張RT上,在草計(jì)算受力的時(shí)候,再去讀取這張RT,和風(fēng)力結(jié)合在一起輸出到頂點(diǎn)位移上。


這樣做的好處在于:首先,相比于以往傳遞參數(shù)給Shader來(lái)計(jì)算碰撞的物體數(shù)量不受限制。如果在Shader里申明了固定數(shù)量的位置、大小等信息來(lái)計(jì)算碰撞,如果實(shí)際需要計(jì)算碰撞的參數(shù)少于申明的參數(shù)則會(huì)浪費(fèi),多了就會(huì)有丟失。另外,我們可以通過(guò)控制我們的盤狀模型的大小、形狀、法線朝向,來(lái)模擬出草和草之間互相擠壓的效果。

↑受到壓力的草會(huì)向周圍專遞壓力,用一個(gè)受力的代理網(wǎng)格來(lái)產(chǎn)生推力,可以避免復(fù)雜的迭代計(jì)算
來(lái)看看我們Render Feature的核心邏輯:首先,定義好正交相機(jī)和RT的參數(shù),在Configure()函數(shù)里,找到打了Player Tag的GameObject,并以它為中心,調(diào)整我們的正交相機(jī)、并獲取正交相機(jī)的透視和投影矩陣。



Execute()里,使用ShaderTag和LayerMask來(lái)限制我們的碰撞模型只渲染在我們申請(qǐng)的這一張RT上。使用Context.DrawRenderers()提交我們的渲染命令。

最后,如果申請(qǐng)了Temporary Render Texture,一定記得要釋放。

Create()和AddRenderPass()算是常規(guī)操作,就不再贅述了。碰撞模型所使用的shader也非常的簡(jiǎn)單。使用從Feature傳出的矩陣做投影變換。然后輸出法線。就是記得Shader的Tag和Feature里定義的保持一致。碰撞模型的GameObject碰撞物體的Layer也要設(shè)置準(zhǔn)確。



如果feature設(shè)置成功的話,現(xiàn)在使用FrameDebugger應(yīng)該可以看到我們已經(jīng)渲了一張RT出來(lái)了。

然后我們回到草的shader部分。我們可以使用此前在Feature里傳遞出來(lái)的相機(jī)參數(shù),還原出采樣RT所用的UV。

采樣之后,我們使用保存在b通道的深度信息和相機(jī)的信息,還原出碰撞模型的世界空間高度。我們可以用它和草頂點(diǎn)的相對(duì)距離,來(lái)計(jì)算RT對(duì)草推力的影響。

現(xiàn)在我們就已經(jīng)獲得了一個(gè)可以交互的草。


除此之外還有一些額外的風(fēng)格化處理:我們讓被碰撞體壓到的草縮小一些。否則在茂密的草叢里,有碰撞體壓到草叢會(huì)讓受力的草戳進(jìn)旁邊的草叢里,看起來(lái)會(huì)稍顯凌亂。


4-讓草的交互富有彈性
到現(xiàn)在我們的草應(yīng)該能滿足一些交互的要求了,但是如果要更近一步,想要草看上去更“物理”、有彈性,我們需要對(duì)生成RT的步驟做進(jìn)一步的處理:

草的受力計(jì)算算是中學(xué)物理的知識(shí),比較簡(jiǎn)單就不再贅述。具體在Feature里實(shí)現(xiàn)的時(shí)候需要注意的一點(diǎn)是:其中計(jì)算當(dāng)前幀的速度、和當(dāng)前幀草的頂點(diǎn)位移時(shí),都需要用到上一幀保存的速度和位移(上一幀碰撞模型的推力可以通過(guò)相機(jī)的一幀之內(nèi)的位移推算出來(lái),所以不需要跨幀保存它)。但是TemporaryRT在每幀結(jié)束都會(huì)執(zhí)行FrameCleanUp,所以速度和最后的位移這兩張RT需要申請(qǐng)普通的RenderTexture。

↑這里的Blit步驟中,Velocity_Temp在使用完之后應(yīng)該可以繼續(xù)用在FinalTmpde的位置。這里為了過(guò)程看起來(lái)清晰就不做修改了。

歡迎加入我們!
感興趣的同學(xué)可以投遞簡(jiǎn)歷至:CYouEngine@cyou-inc.com