衝擊波製作方法探討

gif都給我轉成jpg了我好氣啊。不會動的圖大家點擊一下,彈出窗來就能動了我也是服了。

首先聲明一下,為了gif看起來明顯,所有gif的速度都被我故意調慢了。在實際製作當中,這種效果的速度要快上很多,很多有瑕疵的地方都可以被忽略掉。

衝擊波這個效果也算是個常見效果。基本原理是在RenderToTexture(以下簡寫RT)的某些區域中做UV偏移。很多效果的基本原理都是這個,相信大家也都很清楚怎麼做了。

之所以寫篇文章,是因為這個效果的具體演算法還真是燒掉了我一些腦細胞(數學不及格的鍋)。寫出來做個備份,同時給和我當初一樣有點煩惱的同學一點提示。

假設目前你已經有了RT,有了向Shader當中傳入時間變數的腳本,剩下的事情,就是如何計算UV的偏移。我就從這個地方開始寫起。至於如何獲得到一張RT,可以去百度,網上有很多教程。

首先我們要考慮的問題是如何在一張圖上畫個圓。最開始我使用的方法是直接在ps里做一張圖。然後通過縮放UV的方式來擴大這個圓。

然而這個方法效果很差,使用這種簡單縮放的方式會使整個圈都被等比放大了。而真正擴散的效果,應該如下圖一樣在圓圈擴大的同時,整個圈的寬度並沒有明顯得變化。

所以我放棄了原來的思路,開始思考如何用計算的方式畫出這個圈。

圓其實是相對於圓心等距離的一系列點組成。假X為任意一點,O為圓心,r為半徑的話,則用偽代碼表示的話:

if (length(X , O) == r){ 該點在圓上,輸出顏色值為1(白色);}else{ 該點不在圓上,輸出顏色值為0(黑色);}

如果你想畫一個只有一個像素寬度的圓,這麼寫是沒有問題的。但是如果你想畫出一個有著自定義寬度的圓(比如說上圖中那樣)。那麼就要把寬度問題考慮進去。

畫一個有寬度的圓,實際上是分為兩步進行的。假定圓環的寬度為2d。

第一步,先輸出整個顏色值為1。

第二步,在(r+d)的範圍之外,全部顏色輸出值為0。

第三步,在(r-d)的範圍之內,全部顏色輸出值為0。

看起來是不是很眼熟,是的這就是一個比較大小的工作。如果你寫Shader寫得比較多,一定知道在CG語言當中,比較大小的函數是step()。

step(a, x)等價於:

if(x<a){ return 0;}else{ return 1;}

有的朋友可能會問:既然可以用if來做,為什麼要單獨搞出來一個函數?

因為在GPU上編程和在CPU上有很大的區別。step()函數是單獨的一個指令,比起用if這種分支方法效率高。所以在寫shader的時候盡量不用if/for這些東西。

以上圖為例(OB為圓的半徑,長度為r。BA、BC為圓環寬度的一半,長度為d。X點是在整個圖中的任意一點,與圓心的距離為|OX|

(在實際shader當中,已知中心點UV坐標為(0.5,0.5),X點UV坐標為o.uv,則|OX|的值是distance (o.uv , fixed2(0.5,0.5))。

計算紅色的區域: red = step ((r - d) , |OX|) (如果在紅色區域則返回0,否則返回1)

計算藍色的區域: blue = step (|OX| , (r + d))(如果在藍色區域則返回0,否則返回1)

判斷是否在圓環之中:red * blue

首先,一個點不可能既在紅色區域,又在藍色區域。所以,這個點無論是在紅色區域還是在藍色區域之中,red和blue必然會有一個是0。red * blue結果必然是0。

當blue和red的值都為1的時候(既不在紅色區域,也不在藍色區域當中)。red*blue的結果必然為1。通過這種方法就可以用shader畫出一個給定半徑r和寬度2d的圓。

當半徑r為腳本傳進來的時間變數的時候,這個圓環就會隨著時間的變化而擴大,但是整個圓環的寬度並不會改變,也就是完成了我們之前的需求。

當然,這也帶來了一個明顯的問題——擴散的邊緣顯得十分僵硬。顯然這並不是我們希望看到的,我們更希望看到一個邊緣較為平滑的效果。

這其實就是要在黑白(1和0)之間做一個過渡效果。依然以上上張圖為例。

假定X點和B點重合,那麼X點的值為1;如果X點與A點(或者C點)重合,那麼X點的值為0;X點在BC、BA之間的位置,應該是一個0-1之間的浮點值。

為了方便說明,我們假定X點如圖所示,是在BC之間。那麼X點的值,應該就是XC/BC,換成另外一種寫法就是1 - (|OX| - r) / d。

當然,X點並不一定都在BC之間,也有可能在BA之間。如果是這樣的話按照上面的寫法就會出現負數,會對後面的計算有影響。這裡加上一個取絕對值就可以解決:1 - abs(|OX| - r) / d。

結果如下圖所示。

最後用這個值去做uv的偏移。我使用了最簡單的方式,就是用這個值乘以一個權重直接加到原uv上面。

完整的fragment shader如下(_InTime是腳本傳進來的時間變數,相當於一個不斷變大的半徑值r;_RingWidth是圓環的二分之一寬度d,_Offset是UV偏移的權重):

fixed4 c = 1; fixed dis = distance (o.uv , fixed2(0.5,0.5));fixed mask = step ((_InTime - _RingWidth) , dis) * step (dis , (_RingWidth + _InTime));mask *= 1 - abs(dis - _InTime) / _RingWidth; c = tex2Dproj (_RT , (o.screenPos + mask * _Offset));return c;

其實說了半天,就是講怎麼在Shader裡面畫一個帶過度的圓環。至於還能玩出什麼花樣,比如畫N個圓環,又或者不是圓而是個方形之類的,我想也並不是什麼難事了。

推薦閱讀:

獨立遊戲開發者,還在為啟動資金髮愁么?好消息
如何看待半條命系列對遊戲界的影響?
為什麼遊戲里的鏡頭虛化「尤其是近景人物」總看起來不太自然?
暗黑破壞神2有哪些缺點讓人無法忍受?
《地獄之刃》:電子遊戲對社會與文化領域的全新探索

TAG:shader | Unity游戏引擎 | 游戏开发 |