美文网首页
Unity Shader 深度值重建世界坐标

Unity Shader 深度值重建世界坐标

作者: 雄关漫道从头越 | 来源:发表于2019-08-13 17:28 被阅读0次

    Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)

    根据深度重建世界坐标

    证明世界坐标重建正确的方法

    首先,得先找到一种证明反推回世界空间位置正确的方法。这里,我在相机前摆放几个物体,尽量使之在世界坐标下的位置小于1,方便判定颜色如下图:


    然后将几个物体的shader换成如下的一个打印世界空间位置的shader:

    //puppet_master
    //https://blog.csdn.net/puppet_master  
    //2018.6.10  
    //打印对象在世界空间位置
    Shader "DepthTexture/WorldPosPrint"
    {
        SubShader
        {
            Tags { "RenderType"="Opaque" }
            LOD 100
     
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                
                #include "UnityCG.cginc"
     
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
     
                struct v2f
                {
                    float3 worldPos : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
                
                v2f vert (appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
                    return o;
                }
                
                fixed4 frag (v2f i) : SV_Target
                {
                    return fixed4(i.worldPos, 1.0);
                }
                ENDCG
            }
        }
        //fallback使之有shadow caster的pass
        FallBack "Legacy Shaders/Diffuse"
    }
    

    然后挂上上面的重建世界坐标位置的脚本,在开启和关闭脚本前后,屏幕输出完全无变化,说明通过后处理重建世界坐标位置与直接用shader输出世界坐标位置效果一致:

    逆矩阵方式重建

    深度重建有几种方式,先来看一个最简单粗暴,但是看起来最容易理解的方法:

    我们得到的屏幕空间深度图的坐标,xyz都是在(0,1)区间的,需要经过一步变换,变换到NDC空间,OpenGL风格的话就都是(-1,1)区间,所以需要首先对xy以及xy对应的深度z进行*2 - 1映射。然后再将结果进行VP的逆变换,就得到了世界坐标。

    shader代码如下:

    //puppet_master
    //https://blog.csdn.net/puppet_master  
    //2018.6.10  
    //通过逆矩阵的方式从深度图构建世界坐标
    Shader "DepthTexture/ReconstructPositionInvMatrix" 
    {
        CGINCLUDE
        #include "UnityCG.cginc"
        sampler2D _CameraDepthTexture;
        float4x4 _InverseVPMatrix;
        
        fixed4 frag_depth(v2f_img i) : SV_Target
        {
            float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
            //自己操作深度的时候,需要注意Reverse_Z的情况
            #if defined(UNITY_REVERSED_Z)
            depthTextureValue = 1 - depthTextureValue;
            #endif
            float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depthTextureValue * 2 - 1, 1);
            
            float4 worldPos = mul(_InverseVPMatrix, ndc);
            worldPos /= worldPos.w;
            return worldPos;
        }
        ENDCG
     
        SubShader
        {
            Pass
            {
                ZTest Off
                Cull Off
                ZWrite Off
                Fog{ Mode Off }
     
                CGPROGRAM
                #pragma vertex vert_img
                #pragma fragment frag_depth
                ENDCG
            }
        }
    }
    

    C#部分:

    /********************************************************************
     FileName: ReconstructPositionInvMatrix.cs
     Description:从深度图构建世界坐标,逆矩阵方式
     Created: 2018/06/10
     history: 10:6:2018 13:09 by puppet_master
     https://blog.csdn.net/puppet_master
    *********************************************************************/
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    [ExecuteInEditMode]
    public class ReconstructPositionInvMatrix : MonoBehaviour {
     
        private Material postEffectMat = null;
        private Camera currentCamera = null;
     
        void Awake()
        {
            currentCamera = GetComponent<Camera>();
        }
     
        void OnEnable()
        {
            if (postEffectMat == null)
                postEffectMat = new Material(Shader.Find("DepthTexture/ReconstructPositionInvMatrix"));
            currentCamera.depthTextureMode |= DepthTextureMode.Depth;
        }
     
        void OnDisable()
        {
            currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
        }
     
        void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            if (postEffectMat == null)
            {
                Graphics.Blit(source, destination);
            }
            else
            {
                var vpMatrix = currentCamera.projectionMatrix * currentCamera.worldToCameraMatrix;
                postEffectMat.SetMatrix("_InverseVPMatrix", vpMatrix.inverse);
                Graphics.Blit(source, destination, postEffectMat);
            }
        }
    }
    

    效果如下,重建ok:


    看起来比较简单,但是其中有一个/w的操作,如果按照正常思维来算,应该是先乘以w,然后进行逆变换,最后再把world中的w抛弃,即是最终的世界坐标,不过实际上投影变换是一个损失维度的变换,我们并不知道应该乘以哪个w,所以实际上上面的计算,并非按照理想的情况进行的计算,而是根据计算推导而来(更加详细推导请参考这篇文章,不过我感觉这个推导有点绕)。

    已知条件(M为VP矩阵,M^-1即为其逆矩阵,Clip为裁剪空间,ndc为标准设备空间,world为世界空间):

    ndc = Clip.xyzw / Clip.w = Clip / Clip.w

    world = M^-1 * Clip

    二者结合得:

    world = M ^-1 * ndc * Clip.w

    我们已知M和ndc,然而还是不知道Clip.w,但是有一个特殊情况,是world的w坐标,经过变换后应该是1,即

    1 = world.w = (M^-1 * ndc).w * Clip.w

    进而得到Clip.w = 1 / (M^ -1 * ndc).w

    带入上面等式得到:

    world = (M ^ -1 * ndc) / (M ^ -1 * ndc).w

    所以,世界坐标就等于ndc进行VP逆变换之后再除以自身的w。

    不过这种方式重建世界坐标,性能比较差,一般来说,我们都是逐顶点地进行矩阵运算,毕竟定点数一般还是比较少的,但是全屏幕逐像素进行矩阵运算,这个计算量就不是一般的大了,性能肯定是吃不消的。

    补充:如果是DepthNormalTexture中的depth通过逆矩阵方式重建,计算方式略有不同:

    float depth;
    float3 normal;
     
    float4 cdn = tex2D(_CameraDepthNormalsTexture, i.uv);
    DecodeDepthNormal(cdn, depth, normal);
     
    //float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
    //逆矩阵的方式使用的是1/z非线性深度,而_CameraDepthNormalsTexture中的是线性的,进行一步Linear01Depth的逆运算
    depth = (1.0/depth - _ZBufferParams.y) /_ZBufferParams.x ;
    //自己操作深度的时候,需要注意Reverse_Z的情况
    #if defined(UNITY_REVERSED_Z)
    depth = 1 - depth;
    #endif
     
    float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth * 2 - 1, 1);
    float4 worldPos = mul(_InverseVPMatrix, ndc);
    worldPos /= worldPos.w;
    

    屏幕射线插值方式重建

    这种方式的重建,可以参考Secrets of CryENGINE 3 Graphics Technology这个CryTech 2011年的PPT。借用一张图:

    然后偶再画个平面的图:

    上图中,A为相机位置,G为空间中我们要重建的一点,那么该点的世界坐标为A(worldPos) + 向量AG,我们要做的就是求得向量AG即可。根据三角形相似的原理,三角形AGH相似于三角形AFC,则得到AH / AC = AG / AF。由于三角形相似就是比例关系,所以我们可以把AH / AC看做01区间的比值,那么AC就相当于远裁剪面距离,即为1,AH就是我们深度图采样后变换到01区间的深度值,即Linear01Depth的结果d。那么,AG = AF * d。所以下一步就是求AF,即求出相机到屏幕空间每个像素点对应的射线方向。看到上面的立体图,其实我们可以根据相机的各种参数,求得视锥体对应四个边界射线的值,这个操作在vertex阶段进行,由于我们的后处理实际上就是渲染了一个Quad,上下左右四个顶点,把这个射线传递给pixel阶段时,就会自动进行插值计算,也就是说在顶点阶段的方向值到pixel阶段就变成了逐像素的射线方向。

    那么我们要求的其实就相当于AB这条向量的值,以上下平面为例,三维向量只比二维多一个维度,我们已知远裁剪面距离F,相机的三个方向(相机transform.forward,.right,.up),AB = AC + CB,|BC| = tan(0.5fov) * |AC|,|AC| = Far,AC = transorm.forward * Far,CB = transform.up * tan(0.5fov) * Far。

    我直接使用了远裁剪面对应的位置计算了三个方向向量,进而组合得到最终四个角的向量。用远裁剪面的计算代码比较简单(恩,我懒),不过《ShaderLab入门精要》中使用的是近裁剪面+比例计算,不确定是否有什么考虑(比如精度,没有测出来,如果有大佬知道,还望不吝赐教)。

    shader代码如下:

    //puppet_master
    //https://blog.csdn.net/puppet_master  
    //2018.6.16  
    //通过深度图重建世界坐标,视口射线插值方式
    Shader "DepthTexture/ReconstructPositionViewPortRay" 
    {
        CGINCLUDE
        #include "UnityCG.cginc"
        sampler2D _CameraDepthTexture;
        float4x4 _ViewPortRay;
        
        struct v2f
        {
            float4 pos : SV_POSITION;
            float2 uv : TEXCOORD0;
            float4 rayDir : TEXCOORD1;
        };
        
        v2f vertex_depth(appdata_base v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord.xy;
            
            //用texcoord区分四个角,就四个点,if无所谓吧
            int index = 0;
            if (v.texcoord.x < 0.5 && v.texcoord.y > 0.5)
                index = 0;
            else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
                index = 1;
            else if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
                index = 2;
            else
                index = 3;
            
            o.rayDir = _ViewPortRay[index];
            return o;
            
        }
        
        fixed4 frag_depth(v2f i) : SV_Target
        {
            
            float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
            float linear01Depth = Linear01Depth(depthTextureValue);
            //worldpos = campos + 射线方向 * depth
            float3 worldPos = _WorldSpaceCameraPos + linear01Depth * i.rayDir.xyz;
            return fixed4(worldPos, 1.0);
        }
        ENDCG
     
        SubShader
        {
            Pass
            {
                ZTest Off
                Cull Off
                ZWrite Off
                Fog{ Mode Off }
     
                CGPROGRAM
                #pragma vertex vertex_depth
                #pragma fragment frag_depth
                ENDCG
            }
        }
    }
    

    C#代码如下:

    /********************************************************************
     FileName: ReconstructPositionViewPortRay.cs
     Description:通过深度图重建世界坐标,视口射线插值方式
     Created: 2018/06/16
     history: 16:6:2018 16:17 by puppet_master
     https://blog.csdn.net/puppet_master
    *********************************************************************/
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
     
    [ExecuteInEditMode]
    public class ReconstructPositionViewPortRay : MonoBehaviour {
     
        private Material postEffectMat = null;
        private Camera currentCamera = null;
     
        void Awake()
        {
            currentCamera = GetComponent<Camera>();
        }
     
        void OnEnable()
        {
            if (postEffectMat == null)
                postEffectMat = new Material(Shader.Find("DepthTexture/ReconstructPositionViewPortRay"));
            currentCamera.depthTextureMode |= DepthTextureMode.Depth;
        }
     
        void OnDisable()
        {
            currentCamera.depthTextureMode &= ~DepthTextureMode.Depth;
        }
     
        void OnRenderImage(RenderTexture source, RenderTexture destination)
        {
            if (postEffectMat == null)
            {
                Graphics.Blit(source, destination);
            }
            else
            {
                var aspect = currentCamera.aspect;
                var far = currentCamera.farClipPlane;
                var right = transform.right;
                var up = transform.up;
                var forward = transform.forward;
                var halfFovTan = Mathf.Tan(currentCamera.fieldOfView * 0.5f * Mathf.Deg2Rad);
     
                //计算相机在远裁剪面处的xyz三方向向量
                var rightVec = right * far * halfFovTan * aspect;
                var upVec = up * far * halfFovTan;
                var forwardVec = forward * far;
     
                //构建四个角的方向向量
                var topLeft = (forwardVec - rightVec + upVec);
                var topRight = (forwardVec + rightVec + upVec);
                var bottomLeft = (forwardVec - rightVec - upVec);
                var bottomRight = (forwardVec + rightVec - upVec);
     
                var viewPortRay = Matrix4x4.identity;
                viewPortRay.SetRow(0, topLeft);
                viewPortRay.SetRow(1, topRight);
                viewPortRay.SetRow(2, bottomLeft);
                viewPortRay.SetRow(3, bottomRight);
     
                postEffectMat.SetMatrix("_ViewPortRay", viewPortRay);
                Graphics.Blit(source, destination, postEffectMat);
            }
        }
    }
    

    开关后处理前后效果仍然不变:

    这里我用了默认非线性的深度图进行的深度计算,需要先进行Linear01Depth计算,如果用了线性深度,比如DepthNormalTexture,那么就进行一步简单的线性映射即可。整体的射线计算,我用了Linear01Depth * 外围计算好的距离。也可以用LinearEyeDepth * 外围计算好的方向。总之,方案还是蛮多的,变种也很多,还有自己重写Graphic.Blit自己设置Quad的值把index设置在顶点的z值中。

    屏幕射线插值方式重建视空间坐标

    补充一条屏幕空间深度重建坐标的Tips。如果我们要求视空间的位置的话,有一种更简便并且性能更好的方式。这种方式与上面的屏幕射线插值的方式重建世界坐标的原理一致。只需要输入一个投影矩阵的逆矩阵,即在vertex阶段,从NDC坐标系的四个远裁剪面边界(+-1,+-1,1,1)乘以逆投影矩阵,得到视空间的四个远裁剪面坐标位置,然后除以齐次坐标转化到普通坐标下。这样的四个点的位置也就是视空间下从相机到该点的射线方向,经过插值到fragment阶段直接乘以01区间深度就得到了该像素点的视空间位置了。

    那么就只有一个问题没有解决,在于应该如何获得NDC坐标系下的边界点。上面推导中提到过,在后处理阶段,实际上就是绘制了一个Quad,对应整个屏幕。这个Quad的四个边界点刚好对应屏幕的四个边界点,uv是(0,1)区间的,刚好对应屏幕空间,我们通过*2 - 1将其转化到(-1,1)区间就可以得到四个边界对应NDC坐标系下的xy坐标了。

    v2f vert (appdata v)
    {
        float4 clipPos = float4(v.uv * 2 - 1.0, 1.0, 1.0);
        float4 viewRay = mul(_InverseProjectionMatrix, clipPos);
        o.viewRay = viewRay.xyz / viewRay.w;
        return o;
    }
     
    fixed4 frag (v2f i) : SV_Target
    {
        float depthTextureValue = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        float linear01Depth = Linear01Depth(depthTextureValue);
        float3 viewPos = _WorldSpaceCameraPos.xyz + linear01Depth * i.viewRay;
        
    }
    

    此处的InverseProjectionMatrix与上文中一样,也需要自己传入,因为在后处理阶段,内置矩阵已经被替换了。

    相关文章

      网友评论

          本文标题:Unity Shader 深度值重建世界坐标

          本文链接:https://www.haomeiwen.com/subject/rgvbjctx.html