Dark Sky and Dark River An Example of Adding Object in Ray Marching World

這篇文章是我在 PTT 文章的備份

https://www.ptt.cc/bbs/GameDesign/M.1655569365.A.8E7.html

提要

https://youtu.be/Je88e5KKYJU (沒有魚眼)

https://youtu.be/_RTsKbjj0iw (有魚眼)

本篇也是講 ray marching,主要是講些實作上的小技巧。
如果你打算做的 3D 遊戲有戶外或是看的到天空的場景,
那這篇就挺重要的,請務必看一下。

準備

完整的程式在這:
https://www.shadertoy.com/view/sdVcDG

請務必先搞懂前一篇 (六) 的程式,
如果前一篇有模糊不清的地方,這篇只會更搞不懂的。

天空的構造

這次的世界有二部分,
一個是天空 (sky),一個是河流 (river),
二部分的程式幾乎是一樣的。
為了當教材,我刻意寫二份,
讓他們各自獨立且可以分拆來顯示。

先用 sky 當說明。

前一篇 (六) 的範例,我們有個 world(pos),
它會回傳 pos 這個點的密度。

這次的範例除了密度,還會回傳一個 distance 的值,
那個值是 pos 離天空最近的距離值,
主要的用途是加快 ray marching 的速度。

因為天空是水平平面,
所以最近距離就是 pos 和天空的垂直距離。

再者,最近距離 > 0 時,通常密度就是 0,
要到天空裡面才會有天空密度,這是當然的。

void sky( pos, out distance, out density )

在抽象上,我把天空當成一個高度是 1.0 的無限大的水平平面。

float sky( pos, out distance, out density )
{
// pos 和 sky 的最短距離
distance = 1. – pos.y + fbm(pos*4.-iTime*0.2) * 0.3;
// pos 的密度
density = step(distance, 0.05) * (0.05 – distance);
}

分行說明

一、distance = 1. – pos.y + fbm(pos*4.-iTime*0.2) * 0.3;

(1.0 – pos.y) 的意思可以用簡單的示意圖來說明:

高度 distance density
+3 -2.0 2.05
+2 -1.0 1.05
+1 ——- sky —–
+0.07 0.03 0.02
+0.05 0.05 0
+0 1.0 0
-1 2.0 0
-2 3.0 0

天空以上是負值距離,天空以下是正值距離。

但如果只有 (1.0 – pos.y),
我們只會畫出一個完美的如鏡面般的天空,
而不是畫面看到的凹凸雲朵,所以我們加了 fbm 在後面。

二、density = step(distance, 0.05) * (0.05 – distance);

這行其實等於
if ( distance < 0.05 )
density = 0.05 – distance;
也就是如果高度比天空低超過 0.05 的所有點,密度都是 0。
只有比天空下 0.05 高的點才有密度。

順便講一下為什麼不用 if-statement。

顯示卡 (GPU) 的設計上,
對 if-statement 很不擅長,會拖累效能。
但也不是所有的 if-statement 都有效能問題,
這跟硬體有關,下面的文章有相關討論:
https://bit.ly/3y2snou
不過習慣上能少用就少用,能不用就不用,
這次因為判斷基準是常數(constant) 0.05,
所以好像用了也沒關係… 吧?

前一篇的範例是世界固定不變,
我們移動觀察者 (攝影機) 來造成畫面的變化。
這一篇則是相反,是攝影機不動,
我們讓世界變形來造成畫面的變化。

ray marching

ray marching 的基本邏輯和前一篇差不多。

有一個起始點 pos,pos 會朝 rd 方向前進 20 次,
每次都會取得新點的密度,全部加起來後會回傳總密度。

但細部有些小技巧,逐行說明如下:

一、if ( density_sum > 0.95 ) break;

總密度太大就不用再前進了,這點很正常,
不過為什麼不是 1.0 而是 0.95 呢?
因為我們希望就算是光線經過的密度累積到了極限,
每個 pixel 也會有色差的不同 (0.95 ~ 1.0),
而不是通通都是白白的 1.0。

二、sky(pos, distance, density )

只是取的 pos 與天空的距離和密度,
密度只有 pos 在天空裡面時才有。

三、density_sum += (1. – density_sum)*density;

這行跟前一篇 (六) 的範例不同,
我們不像前一篇那麼直覺的先乘個 0.2 再加總,
而是照比例加在總密度還剩下的空間。
這樣加可以確保總密度不會一下就超過 1,
而且畫出來的效果好很多。

四、二行一起看
distance = max(distance, 0.01 );
pos = pos + rd*distance;

前一篇 (六) 光線明明一次只移動零點零幾的,
為什麼這裡可以移動目前的位置離天空的高度 (distance) 那麼遠?

因為 distance = sky( pos ) 回傳的是垂直高度,
也就是 pos 離天空的最短距離,
既然是最短,
我們 “幾乎” 可以確定這距離不會碰到天空任何一個有密度的點。
至於為什麼是 “幾乎”? 因為天空平面被 fbm 惡搞過。

五、density_sum = pow( density_sum, 0.6 );

這是 gamma correction,
玩攝影的應該都知道,只是調亮度。
為什麼調亮度不直接寫 density_sum += 0.3 ?
因為這樣會讓比 0.7 大的點通通變成爆炸亮,
把細節就通通看不見了,
所以 gamma correction 比較好。

河流的構造

void river( pos, out distance, out density )

河流跟天空的構造完全一樣,
唯一的差別是這行:

density = step(distance, 0.05) * (0.05 – distance) * step(0.0, distance);

也就等於:
if ( distance < 0.05 && distance > 0. )
density = 0.05 – distance;

意思是河流我只取河流水平面以上、高度 0.05 之內的點的密度。
因為很薄,所以可以透過河面看到河底。
當然這次的 shader 沒畫河底出來,
不過透光度的不同就是天空和水的差別。

結語

因為這篇是教學文,
所以河流和天空的 fbm 是一樣的。
但回想前二篇 (五),我們不是有用外框畫水底嗎?
如果用那個當 texture/noise 來構成平靜河流平面,
應該也是很不錯的吧?

好玩的東西

把河流拿掉,void sky() 改成下面的球,
畫面上就會跑出一個球,
然後你可以加點 noise 進去喔,很好玩的。

void ball( vec3 pos, out float distance, out float density )
{
vec3 center = vec3( 0., 0., 1. );
float radius = 0.5;
distance = length(pos-center) – radius;
density = (1. – step(0., distance))*(1.-length(pos-center)/radius);
}