這篇是 Ray Marching 相關系列的最後一篇,
接著看緣份吧,因為越寫越不『超』新手了。
今天我為這篇文章寫的 shader 在這:
https://youtu.be/P4wCIKMwYlE
沒錯,有顏色了,這次的主題就是光,
與如何讓物件疊加的方法。
(畫面裏面有『四個』球)
其實要我寫『光』的教學文,我感覺超心虛的。
我微積分學的不好,很多東西我只會抄,
一堆相關公式文章wiki根本看不懂在說三隻小貓的。
別說我自己寫了,
我還希望有哪個大師寫給我看勒。Orz
所以... 這篇真的是新手文囉。
[基本知識]
這篇『不會』說到像是用 Game Engine 或是 Blender,
先做幾個多邊形,然後擺個光源調個強度,
再改一改材質就做會出來的效果。
這篇是新手文,不是大學電腦繪圖課。:>
這篇會講的是體積光 (Volumetric Light)。
體積光是甚麼?體積光也有人叫他 God Light,長這樣:
https://i.ebayimg.com/images/g/mggAAOSwShpZc31P/s-l500.jpg
早期的電腦繪圖,光影效果只出現在實際存在的鋼體上。
像是實心的球表面有光暗反射散射等等的效果,
影子也只會出現在實體的地面上,
那些也就是我上面說的大學電腦繪圖課會教的東西。
但體積光,就像上面那個天使要降臨前的光束,
是在空無一物的空氣中出現的。
這種東西就像讓光有體積,或是把光當作物體一樣看待,
所以就叫體積光。
我們之前二個 shader,
不都有假定世界任何一個點都有密度嗎?
只要有密度,就能被光照到,
而且密度不是 1,光就能透過去,
並照到下一個點 (ray marching)。
這概念其實就是體積光。
再換個說法,現實中看的到體積光,是因為空氣中有粉塵,
粉塵多寡就代表了該點為中心的某個範圍的密度,
所以是很符合物理現實的。
除了體積光,還需要知道一些光的基本常識:
1. 反射 (Reflection)
就國中物理的入射光反射光那些東西,
這篇 shader 沒用到,再用 GPU 都要燒起來了。
2 吸收 (Absortion)
光射到一個物體,某些波長會被吸收變成熱量之類的,
沒被吸收然後反射的部分就是顏色。
3. 透射 (Transmittance)
光可以穿過透明物體,不過會有耗損 (被反射或被吸收了)
所以,Reflection + Absortion + Transmittance = 1.0
透射就是等等程式裡的密度,
程式中的光的顏色則是代表了吸收的概念。
大家國中物理一定都學得很好,
所以我們開始看程式吧!(笑)
[程式]
程式在這:
https://www.shadertoy.com/view/fdycD3
基本邏輯都是繼承前二篇的,所以看不懂可以先翻前面的看。
[世界構成]
func 跟之前長的一樣
void world( vec3 pos, out distance, out density )
這次世界有四個球,ball_1 ~ ball_4。
雖然前一個範例也有 sky/river 二個物體,
但二者沒有任何交集,顏色只是加起來而已。
這是世界的四個球是有重疊的,
這種情況,密度和最短距離要用正確的方法算。
最短距離:
先分別算 pos 對四顆球的最短距離,
然後在四個數字中取最小的。
min( min( min( dis1, dis2 ), dis3 ), dis4 );
密度:
二種算法,
一種是四個球的密度取最大值。
一種是把四個值加起來。
沒有好壞,是疊加處的感覺不同。
我的感覺是透明度越低的,越適合最大值,反之是疊加值。
二種在程式中都有寫,可以試試。
[用 ray marching 算顏色]
raymarch() 現在不是回傳該方向的總密度,
而是直接回傳該方向的總顏色了。
vec4 raymarch( in vec3 ro, in vec3 rd )
要怎麼算某個方向的總顏色呢?
總顏色跟總密度一樣,
是 raymarch 的路徑上所有的採樣點的顏色的總和,
密度高的點顏色佔的比例高,密度低的比例低,
比例的高底是放在 color.a 的透明度裡面。
計算方法跟總密度的算法幾乎一樣。
color.a *= 0.4; // 跟範例 (六) 把密度調低在加的意思一樣
color.rgb *= color.a; // 照透明度調淡顏色
color_sum += color * (1.0 - color_sum.a);
那要怎麼算出各別的採樣點的顏色呢?
假設採樣點位置是 pos,
首先,我們有個光源位置:
LIGHT_POS = vec3( -2.1, -2.6, 1.2 ),
接著我們可以算出採樣點到光源的方向向量。
然後從採樣點出發,
往光源方向 ray marching 前進某距離,
再算出 pos 到光源的該距離的總密度 t。
t 代表 Transmittance (透射率),
密度高的路線,光比較不容易射到 pos,
密度低的路線,光比較容易射到 pos,
若 t = 0,光線可以沒有任何耗損直接射到 pos。
也就是說,
t 越高,光的影響越低,
t 越低,光的影響越高。
假設光的顏色是 light_color,
我們就依照透射率比例,加光的顏色加在原本該點的顏色上,
如此就是下面 code 的意思:
t = lightmarch( pos, LIGHT_POS );
vec3 light_color = vec3(1.0,0.6,0.3);
vec3 ambient_color = vec3(0.91,0.98,1.05);
color.rgb = color.rgb * (light_color*(1.0-t) + ambient_color);
那... ambient color 是甚麼?
如果有用 Blender 或任何的遊戲引擎做過 3D 遊戲都應該用過,
這是 "如果沒有任何光源時,物體的基本自體發光"。
你可以把 ambient_color = vec3(0.0),
然後就會發現光照不到的地方都變全黑了。
在做 3D 遊戲時,
有 ambient color 其實不太好,
因為不好控制光源效果。
比較好的做法是多放幾個光源,
像 direction light 之類的,
但這只是 shader 範例,所以別在意這個了。
[最佳化]
我們一直都沒討論最佳化的問題,
但當你在做光影時,最佳化的問題是避免不了的。
像是我這個範例,在瀏覽器的 WebGL 跑,
若你拿幾千塊的手機或是平板,
fps 我猜就 10 上下吧。XD
那要怎麼辦呢?
首先,我們真的需要對畫面上所有的方向做 ray marching 嗎?
像前一個例子,做天空時,畫面下半部的點可以通通不做 ray march,
做河流時,畫面上半部也是一樣,馬上就可以減少一半的計算。
而以這個四球例子,一般要先算 ro/rd 是否有和任何物體相交,
有相交再做 ray marching。
甚麼?
怎麼知道一條 ro/rd 的光線有沒有和四顆球相交?
wiki 公式和證明:
https://bit.ly/39AgCMz
看不懂在寫三隻小貓?顆顆,寫成 code 是這樣:
// i remember its from Duke in shadertoy,
// but i can't find the origin post, sorry
bool inter(v3 ro,v3 rd,v3 pos,f radius,v3 pt)
{
v3 pos2ro = (ro - pos);
float b = dot(pos2ro, rd);
float c = dot(pos2ro, pos2ro) - radius*radius;
float d = b*b - c;
pt = ro + rd*(-b - sqrt(d));//第一個交點
return d >= 0.0;
}
不是我寫的,給你參考。
還有,光源的 ray marching 採樣
可是直接讓計算次數以倍數成長的,
到底採樣幾次,才會達到你預期的效果?
還是只採樣最遠的那個採樣點,
然後照比例算透射率就好?
然後,我們真的需要這麼逼真的表面嗎?
不用的話,減少 ray marching 採樣次數,
然後調整 noise 參數找到可接受的就好,
像前一個大河的例子,
從 40 次降到 20 次其實也還行。
最後,如果這不是遊戲的主要重點,
我們是不是該手動降低 fps,
把資源留給遊戲前景呢?
作法只是不用每次都在 main 裏叫 ray marching,
加個 timer 減少畫面的更新。
[結語]
連寫了三篇 ray marching,算是把基本的都講完了。
靠這三篇的基礎,也可以在遊戲中做出很炫的東西了。
美麗的夜空、磅礡的大雨、
深海的景象、宇宙、爆炸、通通都做得出來。
但做得出來跟做的漂不漂亮是二回事就是了,
美感是很吃天份的 (我就沒有),
請多看看其他神人的作品吧。