Unity Shader - 根据片段深度重建片段的世界坐标

141 篇文章 35 订阅
74 篇文章 5 订阅

获取当前绘制片段的世界坐标

在Unity中,获取当前绘制对象中的片段的世界坐标,可以按下列方式:

struct a2v {
	...
	float4 pos : POSITION;
	...
};
struct v2f {
	...
	float4 worldPos : TEXCOORD0;
	...
};
v2f vert(a2v v) {
	v2f o = (v2f)0;
	...
	o.worldPos = mul(unity_ObjectToWorld, v.pos);
	...
	return 0;
}
fixed4 frag(v2f i) : SV_Target {
	i.worldPos ...; // 该片段的世界坐标
}

OK,获取绘制的片段的世界坐标是如此的简单。

那么下面开始实践。

绘制世界坐标

为了方便测试功能正确性,放一些模型,调整好他们的位置、缩放,尽量在unity的1 unit单位范围内。
因为我们要直接用xyz坐标当做颜色绘制出来。所以我们要控制要物体的xyz都尽量在0~1范围内。

模型

先放置一个cube,scaleXYZ都是1,用于作为大小参考物
再放置三个quad,scaleXYZ都是1,三种颜色分别代表:红色:X轴,绿色:Y轴,蓝色:Z轴
在这里插入图片描述

再放一个,小Cube,scaleXYZ缩放都是0.1,因为要将它在0~1的坐标范围内移动,弄小一些就好。
再给这个小Cube弄上材质,材质的shader为下面的代码,用于显示世界坐标的。

// jave.lin 2020.03.10 - Draws the world position
Shader "Custom/DrawWP" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct appdata { float4 vertex : POSITION; };
            struct v2f {
                float4 vertex : SV_POSITION;
                float4 wp : TEXCOORD0;
            };
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.wp = mul(unity_ObjectToWorld, v.vertex);
                return o;
            }
            fixed4 frag (v2f i) : SV_Target { return i.wp; }
            ENDCG
        }
    }
}

好了,那么移动小Cube看看坐标值作为颜色绘制的情况如何
在这里插入图片描述

将小Cube直接放大,看看他的各个角度下的片段颜色
在这里插入图片描述

OK,那么确定正确的世界坐标绘制颜色。

接下来需要对深度缓存中的值都还原到世界坐标。

重构深度缓存的世界坐标

深度缓存如何拿到啊?可以看看我上一篇的:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的方法

下面执行在后处理流程,在后处理获取深度缓存并转换每个像素到世界坐标。
先简单说明:

  • ndc.xyz都是 [-1~1]
  • uv的范围是 [0~1]
  • ndc.xy 可以表示为 uv * 2 - 1
  • ndc.z 的范围 [-1~1] 但实际我们缓存只使用到 [0~1],可以从深度缓存中读取 0~1的数据(我记得好想DX是[0~1],OpenGL是[-1~1]),这里从_CameraDepthTexture.R读取的是ndc.z,但是_CameraDepthNormalsTexture.BA读取的是EyeZ/Far的一个比例值(Linear01Depth),具体可以查看之前写过的一篇:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据

那么开始使用ndc来转换到世界坐标。

列出好几种方法,并简单介绍(下面几种方式在DX中没有问题,但在OpenGL中,第一种ndc to world显示有问题,因为我在android上跑,使用底层渲染API是OpenGL的,直接用第二种方式,性能最好,兼容性也好):

  • ndc to world:frag中使用depth与screen.xy得到ndc,再使用_InvVP变换到world space。
  • camera world position + frustum corner world space ray + linear01depth:frustum corner world space ray从vert传到frag,frag再用相机世界坐标 + 视锥体(截锥体)world space的角射线 * depth比例值。
  • ndc to clip, clip to viewRay, viewRay * linear01depth to viewPos, viewPos to wordPos:vert中先是ndc space到clip space,再就是clip的远截面角落点转为view space的射线viewRay,frag中worldPos = cameraPos + viewRay * linear01depth。

先给相机添加OnRenderImage将深度值转到世界坐标当颜色值绘制

下面所有的代码中的注释都非常详细,推荐看看,本文的正文部分没说的细节,可能在注释里有说明,因为我在写demo时的代码,就将注释写上了,懒得在文章中又在写一遍。

首先挂个下面的脚本,设置好脚本的Camera cam,这个就主相机就好了。

// jave.lin 2020.03.12
using UnityEngine;

public class GetWPosFromDepthScript1 : MonoBehaviour
{
    private Camera cam;
    public Material mat;
    // 这里之所以手动设置,并使用这些变量
    // 而不是用unity内置的,是因为后处理中
    // 部分的矩阵会给替换成一些只渲染一个布满屏幕Quad的正交矩阵信息
    private int _InvVP_hash;        // VP逆矩阵
    private int _VP_hash;           // V矩阵
    private int _Ray_hash;          // Frustum的角射线
    private int _InvP_hash;         // P的逆矩阵
    private int _InvV_hash;         // V的逆矩阵
    private void Start()
    {
        cam = gameObject.GetComponent<Camera>();
        cam.depthTextureMode |= DepthTextureMode.Depth;         // _CameraDepthTexture与_CameraDepthNormalsTexture都测试
        cam.depthTextureMode |= DepthTextureMode.DepthNormals;

        _InvVP_hash = Shader.PropertyToID("_InvVP");
        _VP_hash = Shader.PropertyToID("_VP");
        _Ray_hash = Shader.PropertyToID("_Ray");
        _InvP_hash = Shader.PropertyToID("_InvP");
        _InvV_hash = Shader.PropertyToID("_InvV");
    }
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, mat);
    }
    private void OnPreRender()
    {
        var aspect = cam.aspect;                // 宽高比
        var far = cam.farClipPlane;             // 远截面距离长度
        var rightDir = transform.right;         // 相机的右边方向(单位向量)
        var upDir = transform.up;               // 相机的顶部方向(单位向量)
        var forwardDir = transform.forward;     // 相机的正前方(单位向量)
        // fov = field of view,就是相机的顶面与底面的连接相机作为点的夹角,
        // 我们取一半就好,与相机正前方方向的线段 * far就是到达远截面的位置(这条边当做下面的tan公式的邻边使用)
        // tan(a) = 对 比 邻 = 对/邻
        // 邻边的长度是知道的,就是far值,加上fov * 0.5的角度,就可以求出高度(对边)
        // tan(a)=对/邻
        // 对=tan(a)*邻
        var halfOfHeight = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad) * far;
        // 剩下要求宽度
        // aspect = 宽高比 = 宽/高
        // 宽 = aspect * 高
        var halfOfWidth = aspect * halfOfHeight;
        // 前,上,右的角落偏移向量
        var forwardVec = forwardDir * far;
        var upVec = upDir * halfOfHeight;
        var rightVec = rightDir * halfOfWidth;
        // 左下角 bottom left
        var bl = forwardVec - upVec - rightVec;
        // 左上角 top left
        var tl = forwardVec + upVec - rightVec;
        // 右上角 top right
        var tr = forwardVec + upVec + rightVec;
        // 右下角 bottom right
        var br = forwardVec - upVec + rightVec;

        // 视锥体远截面角落点的射线
        var frustumFarCornersRay = Matrix4x4.identity;
        // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角
        frustumFarCornersRay.SetRow(0, bl);
        frustumFarCornersRay.SetRow(1, tl);
        frustumFarCornersRay.SetRow(2, tr);
        frustumFarCornersRay.SetRow(3, br);

        // 使用GL.GetGPUProjectionMatrix接口Unity底层会处理不同平台的投影矩阵的差异
        // 第二个参数是相对RT来使用的,因为RT的UV.v在一些平台是反过来的,这里传false,因为不需要RT的UV变化兼容处理
        Matrix4x4 p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false);
        Matrix4x4 v = cam.worldToCameraMatrix;
        Matrix4x4 vp = p * v;
        mat.SetMatrix(_InvVP_hash, vp.inverse);
        mat.SetMatrix(_VP_hash, vp);
        mat.SetMatrix(_Ray_hash, frustumFarCornersRay );
        mat.SetMatrix(_InvP_hash, p.inverse);
        mat.SetMatrix(_InvV_hash, v.inverse);
    }
}

脚本中,对viewPortRay每行向量,设置了一个射线,分别第几个射线对应哪个角落,可以在shader中,使用SV_VertexID来取到顶点索引,在对索引绘制:0:红,1:绿,2:蓝,3:黄,shader如下:

            struct appdata {
                float4 vertex : POSITION;
                uint vid : SV_VertexID;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                fixed4 col : TEXCOORD2;
            };
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                const fixed4x4 vcol = {
                    {1,0,0,1},
                    {0,1,0,1},
                    {0,0,1,1},
                    {1,1,0,1},
                };
                // 测试用的,用于辨别后处理的四个顶点
                // 经过测试id:0在左下角,1:左上角,2:右上角,3:右下角
                o.col = vcol[v.vid];

                return o;
             }
             fixed4 frag (v2f i) : SV_Target { return i.col; }

运行效果:
在这里插入图片描述

经过测试id:0在左下角,1:左上角,2:右上角,3:右下角

所以在vert中要拿到对应frustum corner ray(视锥体角射线),直接取:_Ray[v.id]就好了,下面一些使用方式中会用到。

ndc to world

在frag shader使用depthscreenPos.xy得到ndc,再使用_InvVPndc变换到world space

详细的描述是:先在直接在片段着色器,获取深度值的ndcZfloat4 fwp = mul(_InvVP, float4(i.uv * 2 - 1, ndcZ, 1));,再fwp /= fwp.w;后,fwp就是深度片段对应的世界坐标了。

有多种写法:
第一种

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };
            float4x4 _InvVP;
			sampler2D _CameraDepthTexture;
            sampler2D _CameraDepthNormalsTexture;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
  			}
            fixed4 frag (v2f i) : SV_Target {
                // tex2D(_CameraDepthTexture, i.uv) 的是ndcZ
                float ndcZ = (SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                // ndc to world pos => worldPos = _InvVP * ndc;
                float4 fwp = mul(_InvVP, float4(i.uv * 2 - 1, ndcZ, 1));
                fwp /= fwp.w;
                return fwp;
            }

这里为何要将fwp世界坐标乘以它本身的w分量。
可以参考:这篇这个国外文章

原因:

看起来比较简单,但是其中有一个/w的操作,如果按照正常思维来算,应该是先乘以w,然后进行逆变换,最后再把world中的w抛弃,即是最终的世界坐标,不过实际上投影变换是一个损失维度的变换,我们并不知道应该乘以哪个w,所以实际上上面的计算,并非按照理想的情况进行的计算,而是根据计算推导而来。
已知条件( M M M V P VP VP矩阵, M − 1 M^{-1} M1即为其逆矩阵, C l i p Clip Clip为裁剪空间, n d c ndc ndc为标准设备空间, w o r l d world world为世界空间):
n d c = C l i p . x y z w / C l i p . w = C l i p / C l i p . w ndc = Clip.xyzw / Clip.w = Clip / Clip.w ndc=Clip.xyzw/Clip.w=Clip/Clip.w
w o r l d = M − 1 ∗ C l i p world = M^{-1} * Clip world=M1Clip
二者结合得:
w o r l d = M − 1 ∗ n d c ∗ C l i p . w world = M ^{-1} * ndc * Clip.w world=M1ndcClip.w
我们已知M和ndc,然而还是不知道Clip.w,但是有一个特殊情况,是world的w坐标,经过变换后应该是1,即
1 = w o r l d . w = ( M − 1 ∗ n d c ) . w ∗ C l i p . w 1 = world.w = (M^{-1} * ndc).w * Clip.w 1=world.w=(M1ndc).wClip.w
进而得到 C l i p . w = 1 / ( M − 1 ∗ n d c ) . w Clip.w = 1 / (M^{-1} * ndc).w Clip.w=1/(M1ndc).w
带入上面等式得到:
w o r l d = ( M − 1 ∗ n d c ) / ( M − 1 ∗ n d c ) . w world = (M ^{-1} * ndc) / (M ^{-1} * ndc).w world=(M1ndc)/(M1ndc).w
所以,世界坐标就等于ndc进行VP逆变换之后再除以自身的w。

那么继续ndc to world的内容

第二种
不同的是,使用_CameraDepthNormalsTexture.BA解码出来的线性linear01Depth 值,而不是ndc.z,所以我们需要先将它转换到ndc.zndc.z = (1/linear01Depth - _ZBufferParams.y) / _ZBufferParams.x

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
            }
            fixed4 frag (v2f i) : SV_Target {
                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的逆运算
                /* Linear01Depth的逆运算
                // Z buffer to linear 0..1 depth
                inline float Linear01Depth( float z )
                {
                    return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
                }
                Linear01Depth = 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
                (_ZBufferParams.x * z + _ZBufferParams.y) * Linear01Depth = 1
                (_ZBufferParams.x * z + _ZBufferParams.y) = 1/Linear01Depth
                (_ZBufferParams.x * z) = 1/Linear01Depth - _ZBufferParams.y
                z = (1/Linear01Depth - _ZBufferParams.y) / _ZBufferParams.x 
                */
                // 此时的depth是ndcZ:ndc space z value(ndc空间下的z值)
                depth = (1.0/depth - _ZBufferParams.y) /_ZBufferParams.x ;
                // //自己操作深度的时候,需要注意Reverse_Z的情况
                // #if defined(UNITY_REVERSED_Z)
                // depth = 1 - depth;
                // #endif
                
                // 从上面的反Linear01Depth运算,到正确的结果
                // 说明中生成_CameraDepthNormalsTexture时
                // 使用的o.depthNormals.w = COMPUTE_DEPTH_01;深度
                // 就相当于处理了Linear01Depth,不然反运算是不能成功的
                float4 ndc = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, depth, 1);
                float4 worldPos = mul(_InvVP, ndc);
                worldPos /= worldPos.w;
                return worldPos;
            }

运行效果:
在这里插入图片描述

但这种方式都有一个问题,那就是:都有在frag shader里执行float4 worldPos = mul(_InvVP, ndc);,但在后处理的话,就意味着要执行screenW*H格frag shader,执行的片段是很多的。我们接着看看其他更优化的方式。

camera world position + frustum corner world space ray + linear01depth

用相机世界坐标 + 视锥体(截锥体)world space的角射线 * depth比例值。

视锥体的角射线在CSharp脚本传入,视锥体射线:_Ray_Ray有四个角落的射线,分别为:左下角,左上角,右上角,右下角_Ray在vert传入frag插值后使用。

这种方式会比ndc to world的方式要高效很多。
在vert中处理内容就只是索引查找:o.ray = _Ray[v.vid];
在frag中,有只有一次tex2D采样一次加法一次乘法(第二种一次乘法,第一种还多了个除法,这里是演示不同写法而已)

第一种写法:

主要思想是:CameraWorldPos + FrustumCornerRay * (EyeZ/Far)
放一张图的话,大概是这样的:
在这里插入图片描述
GIF来演示,四条FrustomCornerRay的0,1,2,3射线分别对应BLRay,TLRay,TRRay,BRRay
在这里插入图片描述

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                uint vid : SV_VertexID;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 ray : TEXCOORD1;
            };
            float4x4 _Ray;
            sampler2D _CameraDepthTexture;
            sampler2D _CameraDepthNormalsTexture;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.ray = _Ray[v.vid];
            }
            fixed4 frag (v2f i) : SV_Target {
                float eyeZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                // worldPos = cameraWorldPos + frustomConerRay * (eyeZ / far);
                // normalize(i.ray) * (eyeZ / far) == 射线 * (eyeZ / far) == 射线 * (Linear01Depth)
                // 所以我才使用了下面的eyeZ * _ProjectionParams.w的方式,因为_ProjectionParams.w == 1/far
                // float3 wp = _WorldSpaceCameraPos.xyz + normalize(i.ray) * eyeZ; // 与正确结果很相似,但肯定是不对的,因为eyeZ是视图空间下的,而i.ray是世界空间下的射线
                // (eyeZ * _ProjectionParams.w) == Linear01Depth(tex2D(depthTex, i.uv).r)
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * (eyeZ * _ProjectionParams.w);
                return fixed4(wp, 1);
            }

第二种写法,也是目前罗列出来的写法中,效率是最高的写法
都一样的思路,写法不一而已,作参考用,与第一种不同的是frag的内容
主要思想是:CameraWorldPos + FrustumCornerRay * linearEyeRate01Z,用linearEyeRate01Z替换了(EyeZ/Far)

			fixed4 frag (v2f i) : SV_Target {
                float linearEyeRate01Z = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                // worldPos = cameraWorldPos + frustomConerRay * linearEyeRate01Z;
                // 世界坐标 = 相机坐标 + 射线(注意不是方向,无归一化处理,所以向量模是很重要的) * 该深度与远截面的比例值
                // 下面的链接:linear01Depth存的是什么,以及_CameraDepthTexture.r以及_CameraDepthNormalsTexture解码后的深度值有是什么都有说明
                // https://blog.csdn.net/linjf520/article/details/104723859#t21
                // linearEyeRate01Z就是链接中的AH / AC的比例值
                // linearEyeRate01Z = AH / AC == Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv))
                // Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)) == DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), out float outLinear01Depth, out float3 normal)中的outLinear01Depth值
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linearEyeRate01Z;
                return fixed4(wp, 1);
             }

vert : ndcPos to clipPos, clipPos to viewPos, viewRay=viewPos.xyz, frag : viewRay * linear01depth to world position

这种方式是效率最差的方式。但这里做演示,都罗列一下。

  • vertex shader中ndc=float4(v.uv * 2 - 1, 1, 1)ndc转到clip,再使用_InvPclip转到viewview生成viewRay下的射线(与之前的不同,之前的是world space下的射线,这里是view space下的射线),将下viewRay传到frag shader
  • frag shader直接拿到vert shader传来的viewRay,再通过读取深度纹理得到ndc.z,再Linear01Depth(ndc.z)得到线性的深度比例值:linear01Depth来对viewRay做线性缩放,得到view space下的坐标viewPos = i.viewRay * linear01Depth,通过float4 worldPos = mul(_InvV, viewPos);

第一种写法:

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                uint vid : SV_VertexID;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 ray : TEXCOORD1;
            };
            float4x4 _InvVP;
            float4x4 _InvP;
            float4x4 _InvV;
            sampler2D _CameraDepthTexture;
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                float far = _ProjectionParams.z;
                float4 clipPos = float4(v.uv * 2 - 1.0, 1.0, 1.0) * far; // 远截面的ndc * far = clip
                float4 viewRay = mul(_InvP, clipPos);
                o.ray = viewRay.xyz;
             }
            fixed4 frag (v2f i) : SV_Target {
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
                // i.ray == view space Ray, haven't normalized
                float3 viewPos = i.ray * Linear01Depth(depth);
                // 这里的_InvV原本为:UNITY_MATRIX_I_V,但是shader时运行在后处理时
                // 所以MVP都被替换了,所以要用回原来主相机的MVP相关的矩阵都必须外部自己传进来
                float4 worldPos = mul(_InvV, float4(viewPos, 1));
                return worldPos;
             }

第二种写法,只有vertex shader不同:

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                // 两种方式获取(ndc.xy * 0.5 + 0.5)==screenPos
                float4 screenPos = float4(v.uv, 1, 1);       // 方法1
                // float4 screenPos = ComputeScreenPos(o.vertex);  // 方法2
                // screenPos.xy /= screenPos.w;
                float2 ndcPos = screenPos.xy * 2 -1;
                float far = _ProjectionParams.z;
                float3 clipVec = float3(ndcPos, 1) * far;
                float3 viewVec = mul(_InvP, clipVec.xyzz).xyz;
                o.ray = viewVec;
             }

要注意的是,这种写的性能相对第二种来说比较差,因为vertex有矩阵乘法,fragment也有,虽然后处理顶点不多,一个Quad就四个点,但像素是全屏的数量:ScreenW*ScreenH。

最后添加了Timeline控制一下镜头旋转与后处理过渡背景的参数,看看效果
在这里插入图片描述

在正交相机下的深度纹理重构世界坐标

这个应该是unity 2018.3.0f2(我使用的unity版本的坑,后面版本unity应该会修复的)
具体有什么坑,可以看看下面我的shader代码注释里有写得很详细。
或是可以参考之前写的:Unity Shader - 获取BuiltIn深度纹理和自定义深度纹理的数据,只看部分内容:注意Unity正交相机中的深度纹理的编码

关于正交相机(正交投影、矩阵)的相关知识,可以参考:Orthographic Projection

下面是我在正交相机下,实现获取深度的世界坐标(与透视的不一样):

CSharp

// jave.lin 2020.03.12
using UnityEngine;

public class GetWPosFromDepthScript1 : MonoBehaviour
{
    public enum ProjType
    {
        Perspective,
        Orthographic
    }
    public ProjType projType;
    [Range(0, 1)]
    public float alpha = 0.3f;
    public Material mat;
    private Camera cam;
    // 这里之所以手动设置,并使用这些变量
    // 而不是用unity内置的,是因为后处理中
    // 部分的矩阵会给替换成一些只渲染一个布满屏幕Quad的正交矩阵信息
    private static int _InvVP_hash;                 // VP逆矩阵
    private static int _VP_hash;                    // V矩阵
    private static int _Ray_hash;                   // Frustum的角射线
    private static int _InvP_hash;                  // P的逆矩阵
    private static int _InvV_hash;                  // V的逆矩阵

    private static int _Ortho_Ray_hash;             // 正交相机的射线向量
    private static int _Ortho_Ray_Oringin_hash;     // 正交相机的射线起点

    private static int _Alpha_hash;

    static GetWPosFromDepthScript1()
    {
        _InvVP_hash = Shader.PropertyToID("_InvVP");
        _VP_hash = Shader.PropertyToID("_VP");
        _Ray_hash = Shader.PropertyToID("_Ray");
        _InvP_hash = Shader.PropertyToID("_InvP");
        _InvV_hash = Shader.PropertyToID("_InvV");

        _Ortho_Ray_hash = Shader.PropertyToID("_Ortho_Ray");
        _Ortho_Ray_Oringin_hash = Shader.PropertyToID("_Ortho_Ray_Oringin");

        _Alpha_hash = Shader.PropertyToID("_Alpha");
    }
    private void Start()
    {
        cam = gameObject.GetComponent<Camera>();
        cam.depthTextureMode |= DepthTextureMode.Depth;         // _CameraDepthTexture與_CameraDepthNormalsTexture都测试
        cam.depthTextureMode |= DepthTextureMode.DepthNormals;


    }
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, mat);
    }
    private void OnPreRender()
    {
        cam.orthographic = projType == ProjType.Orthographic;

        if (cam.orthographic)
        {
            // 正交的处理
            mat.EnableKeyword("_PROJ_METHOD_ORTHOGRAPHIC");
            mat.DisableKeyword("_PROJ_METHOD_PERSPECTIVE");
            // Camera's half-size when in orthographic mode.
            // https://docs.unity3d.com/ScriptReference/Camera-orthographicSize.html
            // The orthographicSize property defines the viewing volume of an orthographic Camera. In order to edit this size, 
            // set the Camera to be orthographic first through script or in the Inspector. 
            // The orthographicSize is half the size of the vertical viewing volume. The horizontal size of the viewing volume depends on the aspect ratio.
            // 由上面的官方API描述中,可得知,orthograpihcSize是控制 view volume,视图长方体的高度的一半的,orthographicSize = 5,那么view volume height = 10
            // unity 1 unit == 100 pixels,所以view volume height = 10 unit == 1000 pixel
            var halfOfHeight = cam.orthographicSize;
            var halfOfWith = cam.aspect * halfOfHeight;
            //Debug.Log($"size:{cam.orthographicSize}, aspect:{cam.aspect}, hw:{halfOfWith}, hh:{halfOfHeight}");

            // 了解orthographic的投影矩阵,更方便与对参数的应用:http://www.songho.ca/opengl/gl_projectionmatrix.html#ortho

            var upVec = cam.transform.up * halfOfHeight;
            var rightVec = cam.transform.right * halfOfWith;

            // 左下角 bottom left
            var bl = -upVec - rightVec;
            // 左上角 top left
            var tl = upVec - rightVec;
            // 右上角 top right
            var tr = upVec + rightVec;
            // 右下角 bottom right
            var br = -upVec + rightVec;

            // 正交相机的四个角落射线的起点
            var orthographicCornersPos = Matrix4x4.identity;
            // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角
            orthographicCornersPos.SetRow(0, bl);
            orthographicCornersPos.SetRow(1, tl);
            orthographicCornersPos.SetRow(2, tr);
            orthographicCornersPos.SetRow(3, br);

            mat.SetVector(_Ortho_Ray_hash, transform.forward * cam.farClipPlane);
            mat.SetMatrix(_Ortho_Ray_Oringin_hash, orthographicCornersPos);
        }
        else
        {
            // 透视的处理
            mat.EnableKeyword("_PROJ_METHOD_PERSPECTIVE");
            mat.DisableKeyword("_PROJ_METHOD_ORTHOGRAPHIC");

            var aspect = cam.aspect;                // 宽高比
            var far = cam.farClipPlane;             // 远截面距离长度
            var rightDir = transform.right;         // 相机的右边方向(单位向量)
            var upDir = transform.up;               // 相机的顶部方向(单位向量)
            var forwardDir = transform.forward;     // 相机的正前方(单位向量)

            // fov = field of view,就是相机的顶面与底面的连接相机作为点的夹角,
            // 我们取一半就好,与相机正前方方向的线段 * far就是到达远截面的位置(这条边当做下面的tan公式的邻边使用)
            // tan(a) = 对 比 邻 = 对/邻
            // 邻边的长度是知道的,就是far值,加上fov * 0.5的角度,就可以求出高度(对边)
            // tan(a)=对/邻
            // 对=tan(a)*邻
            var halfOfHeight = Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad) * far;
            // 剩下要求宽度
            // aspect = 宽高比 = 宽/高
            // 宽 = aspect * 高
            var halfOfWidth = aspect * halfOfHeight;
            // 前,上,右的角落偏移向量
            var forwardVec = forwardDir * far;
            var upVec = upDir * halfOfHeight;
            var rightVec = rightDir * halfOfWidth;
            // 左下角 bottom left
            var bl = forwardVec - upVec - rightVec;
            // 左上角 top left
            var tl = forwardVec + upVec - rightVec;
            // 右上角 top right
            var tr = forwardVec + upVec + rightVec;
            // 右下角 bottom right
            var br = forwardVec - upVec + rightVec;

            var frustumCornersRay = Matrix4x4.identity;
            // 经shader中顶点颜色赋值后出入到屏幕,可以确定,第0是:左下角,1:左上角,2:右上角,3:右下角
            frustumCornersRay.SetRow(0, bl);
            frustumCornersRay.SetRow(1, tl);
            frustumCornersRay.SetRow(2, tr);
            frustumCornersRay.SetRow(3, br);
            mat.SetMatrix(_Ray_hash, frustumCornersRay);
        }
        // 公共属性
        // 使用GL.GetGPUProjectionMatrix接口Unity底层会处理不同平台的投影矩阵的差异
        // 第二个参数是相对RT来使用的,因为RT的UV.v在一些平台是反过来的,这里传false,因为不需要RT的UV变化兼容处理
        Matrix4x4 p = GL.GetGPUProjectionMatrix(cam.projectionMatrix, false);
        Matrix4x4 v = cam.worldToCameraMatrix;
        Matrix4x4 vp = p * v;
        mat.SetMatrix(_InvVP_hash, vp.inverse);
        mat.SetMatrix(_VP_hash, vp);
        mat.SetMatrix(_InvP_hash, p.inverse);
        mat.SetMatrix(_InvV_hash, v.inverse);
        mat.SetFloat(_Alpha_hash, alpha);
    }
}

可以看到,OnPreRender我添加了一个分支,分别处理正交与透视的逻辑处理。
透视的内容不变,正交的与透视的区别在于:

  • 透视构建frustum的角落点射线,主要思路是:camWorldPos + frustumCornerRay * linear01depth;
  • 正交构建的是一条相机前方射线,与四个角射线起点,主要思路是:camWorldPos + viewVolumeCornerPos + camForwardDir * linear01depth;

Shader

    struct appdata {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        uint vid : SV_VertexID;
    };
    struct v2f {
        float4 vertex : SV_POSITION;
        float2 uv : TEXCOORD0;
        float4 rayOrigin : TEXCOORD1;
    };
    float _Alpha;
    sampler2D _MainTex;
    sampler2D _CameraDepthTexture;
    sampler2D _CameraDepthNormalsTexture;
    v2f vert_orthographic(appdata v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = v.uv;
        o.rayOrigin = _Ortho_Ray_Oringin[v.vid];
        return o;
    }
    fixed4 frag_orthographic(v2f i) {
        // 本人jave.lin 2020.03.14,下面代码运行在unity 2018.3.0f2测试结果,如果其他同学的没有这些问题,大概是unity版本不一致
        #if _METHOD_T1
        // 正交相机下,_CameraDepthTexture存储的是线性值,且:距离镜头远的物体,深度值小,距离镜头近的物体,深度值大,可以使用UNITY_REVERSED_Z宏做处理
        // 透视相机下,_CameraDepthTexture存储的是ndc.z值,且:不是线性的。
        float linear01depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
        // return linear01depth;
        // 正交下的相机,_CameraDepthTexture纹理中存储的是线性比例值
        #if defined(UNITY_REVERSED_Z) // 正交需要处理这个宏定义,透视不用,估计后面unity版本升级后会处理正交的这个宏定义处理吧
        linear01depth = 1 - linear01depth;
        #endif
        // return linear01depth;
        float3 wp = _WorldSpaceCameraPos.xyz + i.rayOrigin.xyz + _Ortho_Ray.xyz * linear01depth;
        return lerp(tex2D(_MainTex, i.uv), float4(wp, 1), _Alpha);
        #endif

        #if _METHOD_T2
        // _CameraDepthNormalsTexture的纹理,正交相机,与透视相机下都没问题一样这么使用
        float linear01depth = DecodeFloatRG (tex2D(_CameraDepthNormalsTexture, i.uv).zw);
        // return linear01depth;
        // 正交下的相机,_CameraDepthTexture纹理中存储的是线性比例值
        // #if defined(UNITY_REVERSED_Z) // 测试发现,即使正交模式下:使用_CameraDepthNormalsTexture的深度也是有处理UNITY_REVERSED_Z宏分支逻辑的,所以这里不需要Reverse Z
        // linear01depth = 1 - linear01depth;
        // #endif
        // return linear01depth;
        float3 wp = _WorldSpaceCameraPos.xyz + i.rayOrigin.xyz + _Ortho_Ray.xyz * linear01depth;
        return lerp(tex2D(_MainTex, i.uv), float4(wp, 1), _Alpha);
        #endif
        
        #if _METHOD_TCOLOR
        return i.col;
        #endif
        
        return tex2D(_CameraDepthTexture, i.uv).r;
    }

效果

在这里插入图片描述

2020.03.15 更新,在实现其他深度相关的效果是,也法线Unity在SIGGRAPH2011有分享过一些基于使用深度实现的特效的内容,文档:SIGGRAPH2011 Special Effect with Depth.pdf
如果多年后,下载不了,链接无效了,可以点击这里(Passworld:cmte)下载(我收藏到网盘了)

他分享的也是用:深度世界坐标 = 相机世界坐标 + 世界坐标下的相机坐标指向远截面四个角落的射线 * 深度比例值01
在这里插入图片描述

总结

透视相机的:还是使用第二种方式兼容性最好(DX,GL都没问题),性能最好。

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.ray = _Ray[v.vid];
            }
            fixed4 frag (v2f i) : SV_Target {
                float linearEyeRate01Z = Linear01Depth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * linearEyeRate01Z;
                return fixed4(wp, 1);
             }

正交相机的,我就只写一种方式吧(就上面那种写法),这种也是性能比较高的写法。

Project

backup : UnityShader_GetWorldPosFromDepthTexTesting_2018.03.12

backup : UnityShader_GetWorldPosFromDepthTexTesting_IncludeOrthoTesting_2018.3.0f2

backup : Simplest_WPosFromDepth_2019_4_30f1

Graph

References

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值