- 前言
這是發生在寫完上一篇 Raymarching 範例程式碼的那一晚,
那一晚我右手端著咖啡,左手敲著鍵盤,沉浸在自己的部落格文章裡,
正當我非常滿意我的文筆時,發現了賣雞排以外的路,喔!不!!
我是說突然想到我是利用一個3D物件(範例使用四邊形),來做為射線終點位置的判斷。
如果我想把射線終點範圍擴大到整個螢幕範圍(就像攝影機濾鏡效果一樣),
是不是也能辦的到?
結果,那一晚 ... 我失眠了。
本篇是補充上一篇從只能對局部範圍,延伸到對整個畫面做 Raymarching 的方法說明,
建議先從上一篇看起會比較好理解。
- 原因
Unity 攝影機濾鏡是在攝影機下掛一個 script,
並在這個 script 內的 MonoBehaviour::OnPostRender() 或 MonoBehaviour::OnRenderImage,
呼叫 Graphics.Blit()。
上一篇的 shader 沒有辦法直接使用的問題就在於透過呼叫 Graphics.Blit(),
導致 shader 內的 unity_ObjectToWorld 值永遠是個 identity。
先讓我們隨便在攝影機上掛一個濾鏡,然後開啟 frame debugger 來看看。
可以觀察到 unity_MatrixVP 永遠是個定值。
模型在繪圖時,會依序執行3個步驟
- 從模型的區域座標轉換到世界座標(根據 Transform Component)
- 轉換到攝影機的相對位置(根據 Camera 的 Transform Component)
- 計算投影到螢幕上的座標(根據 Camera Component)
其中,unity_MatrixVP 就是做第2、3步驟。
因此,可以推測出 Graphics.Blit() 只是做一個定點投影到螢幕上的動作,
因此無法用 unity_ObjectToWorld 算出射線終點的世界座標!
- 解答與實作
該文章是在討論如何在VR環境下,用深度圖算出畫面的世界座標位置。
解決方法是利用深度圖算出畫面每個點與攝影機的相對距離,
再使用攝影機到畫面每個點的射線,算出畫面每個點的位置。
射線的計算方式則是在 script 計算,主要是利用反矩陣求出,
可以試想成從步驟三反向執行到步驟一,求出世界座標在算出射線。
實作程式碼如下:
Script 部分
Script 部分
我使用 CommandBuffer 來做攝影機濾鏡,
這個好處是我可以不必指定將這個 script 掛在攝影機上。
在 LateUpdate() 時,收集攝影機的 view matrix 和 project matrix,反轉和修改後(註1),
指定變數 _ClipToWorld 儲存,給繪圖的材質球使用。
Shader 部分
這個好處是我可以不必指定將這個 script 掛在攝影機上。
在 LateUpdate() 時,收集攝影機的 view matrix 和 project matrix,反轉和修改後(註1),
指定變數 _ClipToWorld 儲存,給繪圖的材質球使用。
Shader 部分
基本上和之前沒太大的差別,主要是在 vertex shader 部分原本是計算世界座標位置,
修改成直接算出射線向量
修改成直接算出射線向量
v2f vert (appdata v) { v2f o; // ... 省略 float4 clip = float4(o.vertex.xy, 0.0, 1.0); o.worldDirection = mul(_ClipToWorld, clip) -_WorldSpaceCameraPos; return o; }
我修改了 shader reconstructing position from depth in vr through projection matrix 的計算方式,
文章是在 VR 裝置下處理, 而我的範例是在 PC 平台,所以做了一點修正。
文章是在 VR 裝置下處理, 而我的範例是在 PC 平台,所以做了一點修正。
- 結論
其實有想過直接在攝影機前面掛一個很大的四方形(像 nGUI 一樣),
但考慮到以後會挑戰結合 3D物件 和 Raymarching 的混合繪圖
還是硬著頭皮去網路上找答案 ...
Raymarching 基礎就先暫時到這裡,接下來會挑戰應用部分!
註1.
這一部分說實在很想用幾個字草草混過,但發現如果不說明,
在很多情況下,會無法正常顯示!
還是只能打開小畫家來畫數字了 ...
先回頭來看繪圖 步驟3. 計算投影到螢幕上的座標
這個的轉換矩陣就是 Camera.main.projectionMatrix ,
根據 Unity 官方說明 它裡面的內容如下

在 Script 裡面,做了這樣的修改
結果,投影矩陣內容變成下圖所示

那他的反矩陣就是

但考慮到以後會挑戰結合 3D物件 和 Raymarching 的混合繪圖
還是硬著頭皮去網路上找答案 ...
Raymarching 基礎就先暫時到這裡,接下來會挑戰應用部分!
註1.
這一部分說實在很想用幾個字草草混過,但發現如果不說明,
在很多情況下,會無法正常顯示!
還是只能打開小畫家來畫數字了 ...
先回頭來看繪圖 步驟3. 計算投影到螢幕上的座標
這個的轉換矩陣就是 Camera.main.projectionMatrix ,
根據 Unity 官方說明 它裡面的內容如下

在 Script 裡面,做了這樣的修改
void LateUpdate() { // ... 省略 Matrix4x4 proj = GL.GetGPUProjectionMatrix(Camera.main.projectionMatrix, true); proj[2, 3] = proj[3, 2] = 0.0f; proj[3, 3] = 1.0f; Matrix4x4 view = Camera.main.worldToCameraMatrix; Matrix4x4 clip = Matrix4x4.Inverse(proj * view) * Matrix4x4.TRS(new Vector3(0, 0, -proj[2, 2]), Quaternion.identity, Vector3.one); }
結果,投影矩陣內容變成下圖所示

那他的反矩陣就是

步驟2 計算與攝影機的相對位置使用 Camera.main.worldToCameraMatrix 沒有做任何變更。
將這兩個轉換矩陣相乘後做反轉,然後在對 Z 軸做位移(移動 (far+near) / (far-near))。
這一整個轉換步驟全部記錄在矩陣變數 _ClipToWorld。
接下來把目光放在矩陣變數 _ClipToWorld,可以分解成

接著,來到 shader 裡面來看
將這兩個轉換矩陣相乘後做反轉,然後在對 Z 軸做位移(移動 (far+near) / (far-near))。
這一整個轉換步驟全部記錄在矩陣變數 _ClipToWorld。
接下來把目光放在矩陣變數 _ClipToWorld,可以分解成

接著,來到 shader 裡面來看
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float4 clip = float4(o.vertex.xy, 0.0, 1.0); o.worldDirection = mul(_ClipToWorld, clip) -_WorldSpaceCameraPos; return o; }
到了這裡,我們已經反推求得步驟2 世界座標和攝影機的相對距離,
但要如何證明,這個相對位置,涵蓋整個可視範圍?
在證明之前,先看看 Unity 官方對攝影機可視範圍的定義(Frustum)。
其中,
var frustumHeight = 2.0f * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var frustumWidth = frustumHeight * camera.aspect;

可以得知,將結果轉為射線,經過光柵化(Rasterizing)後,
射線範圍會涵蓋整個可視範圍(Frustum)。
z 軸為 -1 是因為 OpenGL 和 Unity3D 對 Z 軸方向的定義相反,
因此要為 -1 才能使前後方向正常。
看完之後,可以知道這個方法是在很多前提下才得到的結果。
如果其中一個前提無法滿足,就沒有辦法算出正常結果,
因此需要知道其運作內容,才能根據專案環境,作對應修改。
投影矩陣在繪圖上是一個很重要的因素,
透過修改這個矩陣可以達到許多特殊需求(像水面倒影、光線投影之類)。
若之後沒有什麼新梗,也許可以考慮寫一篇討論投影矩陣的文章。
但要如何證明,這個相對位置,涵蓋整個可視範圍?
在證明之前,先看看 Unity 官方對攝影機可視範圍的定義(Frustum)。
其中,
var frustumHeight = 2.0f * distance * Mathf.Tan(camera.fieldOfView * 0.5f * Mathf.Deg2Rad);
var frustumWidth = frustumHeight * camera.aspect;

可以得知,將結果轉為射線,經過光柵化(Rasterizing)後,
射線範圍會涵蓋整個可視範圍(Frustum)。
z 軸為 -1 是因為 OpenGL 和 Unity3D 對 Z 軸方向的定義相反,
因此要為 -1 才能使前後方向正常。
看完之後,可以知道這個方法是在很多前提下才得到的結果。
如果其中一個前提無法滿足,就沒有辦法算出正常結果,
因此需要知道其運作內容,才能根據專案環境,作對應修改。
投影矩陣在繪圖上是一個很重要的因素,
透過修改這個矩陣可以達到許多特殊需求(像水面倒影、光線投影之類)。
若之後沒有什麼新梗,也許可以考慮寫一篇討論投影矩陣的文章。