在Unity的後處理shader中通過屏幕像素座標和深度貼圖反推世界座標

要通過屏幕像素座標反推世界座標,就要知道世界座標是如何變換爲屏幕座標的。理論上,將世界座標(x, y, z)變換爲(u, v, d)的過程如下:

第一步,將座標點(x, y, z, 1)乘以從世界座標系到相機座標系的轉換矩陣(World-to-Camera 4x4 Matrix),將座標點(x, y, z, 1)變換爲相機空間(Camera Space)座標,轉換後的座標爲(x1, y1, z1, w1),其中w1 = 1。

第二步,將相機空間座標乘以從相機座標系到裁剪空間(Clipping Space)座標系的投影矩陣(Projection 4x4 Matrix),將座標點轉換到裁剪空間,轉換後的座標爲(x2, y2, z2, w2),其中w2 = -z1。在Unity中,如果座標點位於視錐體內(z1 > 0),那麼x2,y2的範圍都是[-z1, z1],z2的範圍是[-z1, 0]。也就是說,我們可以想象這一步是將視錐體“壓扁”成一個半立方體。

第三步,將裁剪空間中的座標(x2, y2, z2, w2)除以w2,得到一個歸一化的座標(x3, y3, z3, 1),也就是說,x3, y3的範圍是[-1, 1],z3的範圍是[0, 1]。 根據攝像機投影的屏幕區域(通常是整個屏幕)和x3, y3,就可以得知這個座標點在屏幕上的位置。z3則是深度。



注意上面兩篇文章裏描述的裁剪空間的z2範圍是[-z1, z1],最後得出的歸一化座標的z3的範圍也是[-1, 1],這和我在Unity中的實驗結果有所不同。

根據以上步驟,假如我們在後處理shader中能夠拿到一個像素的歸一化座標(包括深度),並且得知w2,那就可以一步一步反推出世界座標:先將歸一化座標乘以w2轉換到裁剪空間,再乘以投影矩陣的逆轉換回相機空間,最後再乘以世界座標系到相機座標系的轉換矩陣的逆——也就是相機座標系到世界座標系的轉換矩陣,就反推出了世界座標。

不過實際在Unity的後處理shader中,我們往往只能拿到像素的歸一化座標,拿不到w2。因此我們要用另外的辦法。一般我們在後處理shader中,能拿到的是x3, y3, z3,屏幕的高寬,以及相機的near, far和Field of View(FOV)。有了這些信息,我們就有辦法將屏幕座標直接變換到相機空間的座標,而無需得知w2和投影矩陣的逆。

後處理shader的代碼如下:

Shader "Custom/CalcWorldPosByDepthUseDepthTexInPostProcess" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass{
            CGPROGRAM

            #include "UnityCG.cginc"
            #pragma vertex vert_img
            #pragma fragment frag

            sampler2D _CameraDepthTexture;

            float4 GetWorldPositionFromDepthValue( float2 uv, float linearDepth ) 
            {
                float camPosZ = _ProjectionParams.y + (_ProjectionParams.z - _ProjectionParams.y) * linearDepth;

                // unity_CameraProjection._m11 = near / t,其中t是視錐體near平面的高度的一半。
                // 投影矩陣的推導見:http://www.songho.ca/opengl/gl_projectionmatrix.html。
                // 這裏求的height和width是座標點所在的視錐體截面(與攝像機方向垂直)的高和寬,並且
                // 假設相機投影區域的寬高比和屏幕一致。
                float height = 2 * camPosZ / unity_CameraProjection._m11;
                float width = _ScreenParams.x / _ScreenParams.y * height;

                float camPosX = width * uv.x - width / 2;
                float camPosY = height * uv.y - height / 2;
                float4 camPos = float4(camPosX, camPosY, camPosZ, 1.0);
                return mul(unity_CameraToWorld, camPos);
            }

            float4 frag( v2f_img o ) : COLOR
            {
                float rawDepth =  SAMPLE_DEPTH_TEXTURE( _CameraDepthTexture, o.uv );
                // 注意:經過投影變換之後的深度和相機空間裏的z已經不是線性關係。所以要先將其轉換爲線性深度。
                // 見:https://developer.nvidia.com/content/depth-precision-visualized
                float linearDepth = Linear01Depth(rawDepth);
                float4 worldpos = GetWorldPositionFromDepthValue( o.uv, linearDepth );
                return float4( worldpos.xyz / 255.0 , 1.0 ) ;  // 除以255以便顯示顏色,測試用。
            }
            ENDCG
        }
    } 
}


在上面的代碼中,frag函數中的o.uv是將取值範圍轉換到[0, 1]後的x3, y3。_CameraDepthTexture即深度貼圖,裏面存儲的就是每個像素點的z3。爲了使用深度貼圖,需要在C#腳本中將相機的depthTextureMode 爲Depth或者DepthNormal:


MyCamera.depthTextureMode = DepthTextureMode.Depth;  //使用相機自己生成的 _CameraDepthTexture 必須設置這個


unity_CameraProjection是相機的投影矩陣,裏面的第2行第2個元素存儲的就是相機FOV的一半的正切值(tan)。


如何測試計算結果的正確性呢?我們可以在物體自身的材質上寫一個shader,像後處理shader一樣根據世界座標顯示物體的顏色:


Shader "Custom/GenerateDepthAndShowWoldPos" {
    Properties {
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Blend  Off
        


        Pass{
            CGPROGRAM

            #include "UnityCG.cginc"
            #pragma vertex vert
            #pragma fragment frag


            struct v2f {
                float4 pos: SV_POSITION;
                float4 worldpos : TEXCOORD0;
            };

            v2f vert( appdata_img v ) 
            {
                v2f o;
                o.pos = mul( UNITY_MATRIX_MVP,  v.vertex ) ;
                o.worldpos = mul(unity_ObjectToWorld, v.vertex);
                o.worldpos.w = o.pos.z / o.pos.w;
                return o;
            }

            float4 frag( v2f o ) : COLOR
            {
                return float4( o.worldpos.xyz / 255.0, 1.0) ; // o.worldpos.xyz/255 是爲了顏色輸出。 
            }

            ENDCG
        }
    } 
    FallBack "Diffuse"
}


我們知道Unity編輯器的Scene視圖是沒有後處理效果的,而在編輯器中運行遊戲時的Game視圖是有後處理效果的。因此如果Scene和Game視圖中的物體顏色一致,那就說明後處理反推世界座標的邏輯寫對了:




在上圖的Game視圖中,物體以外的背景呈現彩色,是因爲後處理shader會處理屏幕上的所有像素並反推其世界座標。不在物體上的像素全都會被映射到視錐體的far截面上。


注:實驗用的Unity版本是5.5.0p4。本文參考了前同事的一篇筆記:http://note.youdao.com/share/?id=7350142fadd3b244a80df594ddfbb9f2&type=note#/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章