Unity Shader 深度值重建世界坐标

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();
    }
 
    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();
    }
 
    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 深度值重建世界坐标)