根据深度信息重建屏幕像素在世界中的坐标

谈起深度信息,我第一个反应就是陈嘉栋大大写的# 神奇的深度图:复杂的效果,不复杂的原理,最近看到SSAO技术里面也会用到深度信息,去重建屏幕上像素在世界空间下的坐标,阅读相关代码时看到重建这个信息的代码很简单:

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;

就是采样深度图,将得到的数据对应到[-1,1]之间(ndc坐标是在这个区间),然后乘以一个vp的逆矩阵,最后除以w就好了。那么,这到底怎么来的,这篇博客就来推导下。

首先对于这个问题,我们有哪些已知信息,这是很重要的。现在已知屏幕上的像素坐标(用xy或者uv表达都行),深度信息z也是已知的,vp矩阵已知则其逆矩阵也已知。到此,我们尝试推导下能不能重建世界坐标。

已知:\frac{clip_{xyz}}{clip_w} = ndc_{xyz}

vp^{-1} * clip_{xyzw} = world_{xyzw}

ndc下的xyz现在已知,那么我们只需要知道clip_w就能知道像素所对应的剪裁空间坐标,那么得到世界空间坐标就易如反掌了。

现在问题来了,根据上面的已知条件,似乎没法知道clip_w是多少啊。。。

这里其实我们已知的信息除了上面所说外,还有一个很重要的信息,就是世界空间坐标下w=1。有了这个以后,我们就能继续推导了。

已知:vp^{-1} * clip_{xyzw} = world_{xyzw}world_w = 1

那么:vp^{-1} * (clip_{xyz},clip_w) = world_{xyzw}
=> vp^{-1} * (ndc_{xyz} * clip_{w} ,clip_w) = world_{xyzw}
=> clip_{w} * vp^{-1} * (ndc_{xyz},1) = world_{xyzw} (式子1)

到了式子1这一步,世界空间下w=1的功能就要发挥出来了。

已知:world_w = 1

那么:clip_{w} * (vp^{-1} * (ndc_{xyz},1)) _w = 1

为什么会这样呢?clip_{w}是个标量,vp^{-1} * (ndc_{xyz},1)是个vector4,有xyzw四个分量,其中w的分量乘以clip_{w}是1,以对应世界空间下world_w = 1,所以有了式子1

所以:clip_{w} = \frac{1}{(vp^{-1} * (ndc_{xyz},1))_w}

现在clip_{w}知道了,将它代入式子1就可以算出世界坐标了。

最终:world = \frac{vp^{-1} * (ndc_{xyz},1)}{(vp^{-1} * (ndc_{xyz},1)_w}

现在代码中的为什么要这么写就应该很清楚了。

2020.11.26更新
上面的方法虽然直观,但涉及到矩阵运算,效率上会有些影响。那么有没有不涉及矩阵运算也能得出世界坐标的方法呢?有!具体代码如下

float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, UnityStereoScreenSpaceUVAdjust(i.uv, _CameraDepthTexture_ST));
float3 wp = _WorldSpaceCameraPos.xyz + i.ray * Linear01Depth(depth);

我们需要一个相机在世界坐标下的位置,深度信息,以及射线信息就能得出世界坐标了。那么有人要问了,深度信息我知道,可以从深度图中获取,相机信息引擎也提供,那么这个射线信息是什么鬼呢?还有为什么这样可以得出世界坐标呢?
我们先来解决一个问题,上面这个式子怎么来的。根据Secrets of CryENGINE 3 Graphics Technology这个PPT来看,我们可以这么来理解。

上面是PPT中的原图,下面我自己手绘了一下二维版的,这样理解起来就相对容易。

A作为摄像机,BC为远剪裁面,前面那条竖线是近剪裁面(忘记标记名字了= =),我们需要算出F点的位置,那么容易得到F = A + AFAF为向量,F即为我们想要计算的世界坐标),现在AF该怎么计算呢?我们知道,如果将深度限制在01空间中时,AD=1,所以上面代码在使用深度时使用Linear01Depth将深度转换到01空间下。并且由相似三角形定理得AG / AD = AF / AE,现在AD=1,那么AF = AG * AEAG即为01空间的深度d,那么AF = AE * d。现在问题转化为AE怎么求,而这个AE代表着摄像机到屏幕上每个像素的射线。针对屏幕后处理来说,我们实际上只是在渲染一个Quad,那么我们只需要通过摄像头上的一些参数,将摄像机屏幕上的四个顶点计算出来,然后利用光栅化的插值功能,即可得到我们想要的射线AE了。

C#计算四个顶点的代码为

        cam = GetComponent<Camera>();
        cam.depthTextureMode |= DepthTextureMode.Depth;
        camTrans = cam.transform;

        Matrix4x4 frustumCornors = Matrix4x4.identity;
        float fov = cam.fieldOfView;
        float near = cam.nearClipPlane;
        float far = cam.farClipPlane;
        float aspect = cam.aspect;

        float fovWHalf = fov * 0.5f;

        Vector3 toRight = camTrans.right * near * Mathf.Tan (fovWHalf * Mathf.Deg2Rad) * aspect;
        Vector3 toTop = camTrans.up * near * Mathf.Tan (fovWHalf * Mathf.Deg2Rad);

        Vector3 topLeft = (camTrans.forward * near - toRight + toTop);
        float camScale = topLeft.magnitude * far/near;

        topLeft.Normalize();
        topLeft *= camScale;

        Vector3 topRight = (camTrans.forward * near + toRight + toTop);
        topRight.Normalize();
        topRight *= camScale;

        Vector3 bottomRight = (camTrans.forward * near + toRight - toTop);
        bottomRight.Normalize();
        bottomRight *= camScale;

        Vector3 bottomLeft = (camTrans.forward * near - toRight - toTop);
        bottomLeft.Normalize();
        bottomLeft *= camScale;

        frustumCornors.SetRow(0, bottomLeft);
        frustumCornors.SetRow(1, bottomRight);
        frustumCornors.SetRow(2, topRight);
        frustumCornors.SetRow(3, topLeft);

        mat.SetMatrix("_Ray", frustumCornors);

Shader这边只要这样处理就行了

SubShader {
        ZWrite Off 
        ZTest Always 
        Cull Off
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
           
            #include "UnityCG.cginc"
            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 ray : TEXCOORD1;
            };
            
            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv,_MainTex);
                o.uvDepth = v.uvDepth;

            
                int index = 0;
                if (v.uv.x < 0.5 && v.uv.y < 0.5){
                    index = 0;
                }else if (v.uv.x > 0.5 && v.uv.y < 0.5){
                    index = 1;
                }else if (v.uv.x > 0.5 && v.uv.y > 0.5){
                    index = 2;
                }else{
                    index = 3;
                }
             

                o.ray = _Ray[index].xyz;
                return o;
            }
            fixed4 frag (v2f i) : SV_Target {
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, UnityStereoScreenSpaceUVAdjust(i.uv, _CameraDepthTexture_ST));
                float3 wp = _WorldSpaceCameraPos.xyz + i.ray * Linear01Depth(depth);

                return fixed4(wp,1.0);
            }
            ENDCG
        }
    }

参考
Unity Shader 深度值重建世界坐标
Unity Shader-深度相关知识总结与效果实现(LinearDepth,Reverse Z,世界坐标重建,软粒子,高度雾,运动模糊,扫描线效果)
How to go from device coordinates back to worldspace in OpenGL (with explanation)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352