- 前言
講了一篇的前言,終於到了 Unity 實作階段,比較起上一篇用快速傅立葉轉換做水波生成,
文章撰寫的速度快很多!
果然少了複雜的數學公式,不論對我的寫作動力還是網路硬碟容量,都是百利無一害!
(都是數學的錯!我要求道歉!下台!! ... 但不解釋又無法說明原理 )
本篇會說明如何在 Unity 上使用 Raymarching,
實作是使用 Shader 來運算,因此需要對 Unity Shader 有一定的理解。
- Unity 實作
在上一篇談到實作 Raymarching 需要知道
- 所有的射線資料
- 所有障礙物的資料(包含座標、形狀、顏色等 ...)
先從一個簡單的範例來看
這段 Shader Code 主要是使用 Raymarching 在畫面畫出一個球。

先來看看實作上遇到的第一個問題:如何得到所有的射線資料?
射線需要起點和終點,
根據 Raymarching 原理,
起點位置就是攝影機位置,終點位置就是可視範圍(Frustum)的所有點。
因此,只要在 vertex shader 紀錄節點的世界座標,經過 Rasterizing (光柵化) 後,
在 fragment shader 內就可以取的可視範圍內所有點的世界座標。
得到了射線資料後,接下來要知道障礙物資料。
這一段記錄著障礙物資料
仔細一看,這不就是計算任何一點到圓的最短距離的演算法!
如果,我修改這個演算法,改成計算任何一點到環面(Torus)的最短距離
結果得到

從以上說明,可以知道:
在這裡,我推薦 iquilezles.org 這個網頁,
裡面記載了許多障礙物形狀的 distance function,並說明如何顯示兩種以上的障礙物,
和交集、聯集、邊緣平滑化等 ... 方法。
最後,需要額外提及這一段程式碼
這一段就是根據 Raymarching 起點位置到障礙物的最短距離,
判斷是否小於一個門檻值(範例使用 0.001),
當大於門檻值,Raymarching 起點位置往射線方向移動
當小於門檻值,回傳顏色資料並跳出迴圈
這裡的 Raymarching 移動,我設定做最大16次的計算,
這個數值越大,則計算的形狀越準確,但會增加效能負擔。
可以根據需求作修改。
在計算法線,是使用這個演算法
這一段是對射線和障礙物發生衝突的位置,對X、Y、Z軸作微分,計算出三軸方向的斜率。
來求得該點的法線向量近似值。
由範例可以得知,在做 Raymarching 就是把 shader 當 script 來寫,
需要提供圓心座標、障礙物半徑(可以寫死,或透過其他方式傳入)。
用 for 迴圈計算,必要時用到 if - else 判斷,
這些對於從以前就開始寫 shader 的人,是很難去接受的觀念。
但GPU的效能已經不同以往了,不再是那麼的經不起摧殘。
目前有些遊戲都將複雜的浮點數運算都交由GPU處理(上一篇的快速傅立葉轉換也是)。
所以該是放棄成見,擺脫舊觀念,放手去操GPU的時機了。

先來看看實作上遇到的第一個問題:如何得到所有的射線資料?
射線需要起點和終點,
根據 Raymarching 原理,
起點位置就是攝影機位置,終點位置就是可視範圍(Frustum)的所有點。
因此,只要在 vertex shader 紀錄節點的世界座標,經過 Rasterizing (光柵化) 後,
在 fragment shader 內就可以取的可視範圍內所有點的世界座標。
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float3 center : TEXCOORD1;
float3 posWorld : TEXCOORD2;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz;
o.posWorld = mul(unity_ObjectToWorld, v.vertex).xyz;
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// ... 省略
float3 cameraPos = _WorldSpaceCameraPos.xyz;
float3 worldPos = i.posWorld.xyz;
float3 ray = normalize(worldPos - cameraPos);
// ... 省略
}
得到了射線資料後,接下來要知道障礙物資料。
這一段記錄著障礙物資料
/*
* pos = 目前 Raymarching 的起點位置
* center = 球的圓心座標
* size = 球的半徑
*/
float dist_func(float3 pos, float3 center, float size)
{
return length(pos - center) - size;
}
仔細一看,這不就是計算任何一點到圓的最短距離的演算法!
如果,我修改這個演算法,改成計算任何一點到環面(Torus)的最短距離
/*
* pos = 目前 Raymarching 的起點位置
* center = 環面的圓心座標
* size = 環面的半徑(外側,內側)
*/
float dist_func(float3 pos, float3 center, float2 size)
{
float3 p = pos - center;
float2 q = float2(length(p.xy)-size.x,p.z);
return length(q)-size.y;
}
結果得到

從以上說明,可以知道:
- 障礙物的形狀,可以透過計算任何一點到障礙物的最短距離求得
- 計算最短距離的函式,通常稱做 distance function
在這裡,我推薦 iquilezles.org 這個網頁,
裡面記載了許多障礙物形狀的 distance function,並說明如何顯示兩種以上的障礙物,
和交集、聯集、邊緣平滑化等 ... 方法。
最後,需要額外提及這一段程式碼
for(int i = 0; i < 16; ++i)
{
float D = dist_func(cur, centerPos, sphereSize);
if(D < 0.0001)
{
float3 normalDirection = getNormal(cur, centerPos, sphereSize);
float NdotL = dot(normalDirection, lightDirection);
col.rgb = NdotL + 0.1;
col.a = 1;
break;
}
cur += ray * D;
}
這一段就是根據 Raymarching 起點位置到障礙物的最短距離,
判斷是否小於一個門檻值(範例使用 0.001),
當大於門檻值,Raymarching 起點位置往射線方向移動
當小於門檻值,回傳顏色資料並跳出迴圈
這裡的 Raymarching 移動,我設定做最大16次的計算,
這個數值越大,則計算的形狀越準確,但會增加效能負擔。
可以根據需求作修改。
在計算法線,是使用這個演算法
/*
* pos = 目前 Raymarching 的起點位置
* center = 環面的圓心座標
* size = 環面的半徑(外側,內側)
*/
float3 getNormal(float3 pos, float3 center, float2 size)
{
float ep = 0.0001;
float D = dist_func(pos, center, size);
return normalize(
float3 (
D - dist_func(pos - float3(ep,0,0), center, size),
D - dist_func(pos - float3(0,ep,0), center, size),
D - dist_func(pos - float3(0,0,ep), center, size)
)
);
}
這一段是對射線和障礙物發生衝突的位置,對X、Y、Z軸作微分,計算出三軸方向的斜率。
來求得該點的法線向量近似值。
- 總結
由範例可以得知,在做 Raymarching 就是把 shader 當 script 來寫,
需要提供圓心座標、障礙物半徑(可以寫死,或透過其他方式傳入)。
用 for 迴圈計算,必要時用到 if - else 判斷,
這些對於從以前就開始寫 shader 的人,是很難去接受的觀念。
但GPU的效能已經不同以往了,不再是那麼的經不起摧殘。
目前有些遊戲都將複雜的浮點數運算都交由GPU處理(上一篇的快速傅立葉轉換也是)。
所以該是放棄成見,擺脫舊觀念,放手去操GPU的時機了。
沒有留言:
張貼留言