Simplest Ray Marching A shader that show the simplest ray marching idea

這篇是的 PTT 的文章的備份。https://www.ptt.cc/bbs/GameDesign/M.1655569365.A.8E7.html

綱要

對古早的 3D 繪圖來說,
因為受限運算速度,
大家只在乎 “光線碰到某個實體物體表面” 的位置,
換言之,那類運算的假設是光線是不會穿過物體。

但是要畫出像雲,霧,水,煙,火等等這些東西,
光線是會穿過去的,而且顏色是會疊加的,
拿畫一顆球或是立方體的方法畫那類物體,
很難做出真實感。

所以 ray marching 的技術就發展出來了。

準備

這次的 code 我已經寫好,放在:
https://www.shadertoy.com/view/7syyWG

前半部是別人 (iq大大) 的 3d noise,
float noise( vec3 x ) 是給個位置,回傳 0~1 的 noise。
float fbm( vec3 x ) 也是一樣,只是會傳回更複雜 (碎形) 的 noise。

用下面的 main func 可以畫出 fbm 出來看。
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (2.0*fragCoord-iResolution.xy)/iResolution.y;
float n = fbm(vec3(uv,1.));
fragColor = vec4(vec3(n), 1.);
}

世界構造

有二個常用的觀點可以用來看 3d noise 的意義。

  1. pos.xy 是 2d 世界 (x,y) 的一個點,而 pos.z 是時間軸。
  2. pos.xyz 是存在在 3d 世界 (x,y,z) 的一個點。

我們這次用的是後者的觀點。

對後者來說,
世界上的每一個點都是一個 0~1 的數字,
如果把數字這當密度來看,
世界就是一團團不均勻的 “煙霧”。

下面的程式是在這團煙霧裡開一個隧道。

float world( vec3 pos )
{
return fbm(pos * 4.)
– clamp(0.8 – length(pos.xy), 0., 1.);
}

– pos*4: 讓 fbm 縮小 25%。
– 0.8 – length(pos.xy): 如果離中心點 (0,0) 的距離超過 0.8,
該值就是負的。
– clamp: 讓負數變 0。

當 pos 越靠近隧道中心,值就越小。
如果 pos 離中心超過 0.8 以後,值就不受影響,
這樣就像是在煙霧中開了一個隧道了。

觀察者的位置,與每一個uv對應的光線向量

請搭配程式看以下說明。
void mainImage( out vec4 fragColor, in vec2 fragCoord )

觀察者位置其實就是攝影機的位置,
想像攝影機要取得任一個 pixel 的顏色,
就需要往那方向射出一道光線 (物理上是相反的就是了)

在 main 裡,觀察者是 ro,一開始在 (0,0,0)
但會隨時間往前行進 i.e. (0,0,-Time*2)

每個 pixel 的光線向量是 rd,
本來應該是 (uv.x, uv.y, 1.)
但我想讓畫面 (或說攝影機) 一直轉,
所以多了乘 rotateZ (對 Z 軸轉不停),
用 matrix 旋轉、縮放、扭曲、移動向量是數學,
看不懂請翻書。:>

接著我們射出一條光線,取得光線方向的噪音,
也就是:
float density = raymarch( ro, rd );

最後隨便上色,就完成了,簡單吧。:)

ray marching

接下來講光線射出去是要怎麼計算。

請搭配著程式看以下說明。

float raymarch(ro,rd)

因為光線會透過物體,
我們可以每隔一段距離,就取得一個值,
然後走了很多次後,把每個值加起來。

這就跟光一樣,
穿過濃一點的霧,顏色就顯現的明顯些。
穿過淡一點的霧,可以透過去的部分就多一些。
我們要做的是算出某段距離的霧的密度,
然後也就代表 pixel 的霧的總密度了。

回到 code,一開始密度 (density) 為 0,
光線預計採樣 20 次,
每次採樣點距離 0.1,
光線起點是 ro,
每走一步就是 ro + ra*0.1 (往光線方向前進0.1)

如果該點密度太低 (<0.01),
我們就當該點沒東西,讓畫面不要髒髒的。

因為每一點都是 0~1 之間,
隨便加一下就超過 1 了,
所以要 *0.2 讓他加慢一點。

加完回傳就是密度總值了。