Unity shader 获取深度的详细数学原理

需要从shader 中获取深度值,主要涉及到渲染流水线中的以下几个节点

1.观察空间:观察空间是以摄像机所在位置为原点的空间。我们尝试获取的深度信息就是在这个空间之下。

2.裁剪空间(投影空间、齐次裁剪空间):将顶点转换到此空间下来判断是否在视椎体内,从而裁剪掉不在摄像机视野内的顶点;将顶点变换到此空间下可以方便做后续的投影工作。

3.屏幕空间:将没有被裁剪的顶点从裁剪空间转换到此空间,从而最终呈现在屏幕上。


主要涉及到的术语

1.裁剪矩阵(投影矩阵):

用于将顶点从观察空间转换到裁剪空间的矩阵。

经过裁剪矩阵的操作后,顶点的x、y、z分量则被转换到裁剪空间中。

裁剪空间下的顶点满足以下条件的,即为在视野内的顶点:

-w<=x<=w;

-w<=y<=w;

-w<=z<=w;

以上裁剪原理与深度获取关系不大,只需要注意:

经过裁剪矩阵的操作后,顶点的w 分量保存了顶点在观察空间下的深度信息(观察空间的顶点的z 分量)。


2.齐次除法(透视除法):

经过裁剪矩阵的操作之后,将裁剪空间下的顶点的x、y、z 分量分别除以w 分量的过程。此过程完成后,裁剪空间下的顶点将会转换到NDC 中,即变换到一个各分量从-1到1长度为2的立方体内部。如果使用的是透视摄像机,因为z 分量保留了深度信息,经过透视除法以后,越远离摄像机的顶点其xy分量数值将越小(因为w 分量是观察空间中顶点的z 分量,对于两个顶点,如果他们x、y 分量相同,而w 分量不同即深度不同,越远的顶点经过透视除法后,其数值也就越小)。齐次除法完成后,顶点的x、y 分量再经过简单的缩放映射即可投射到屏幕空间二维坐标中,而z 分量通过d = 0.5*z + 0.5 转变为0-1 范围并作为最终会存贮于深度图中的数据。


3.屏幕空间:

渲染管线中最后一个流程将裁剪空间中的顶点通过透视除法和屏幕映射映射最终从3D 的裁剪空间转换到2D 的空间中,这个2D 空间就是屏幕空间。屏幕空间是左下角为(0,0),右上角为(screenwidth, screenheight)的二维空间。


Shader 中获取深度(unity 2019.4.19 urp)

Shader 中将顶点从裁剪空间转换到屏幕空间(即齐次除法和屏幕映射)由底层完成,而获取深度的原理就是再现这一操作中某些步骤的过程。

通过顶点数据可以直接获得顶点的深度;通过顶点数据和屏幕映射公式可以逆推获得屏幕纹理坐标,使用屏幕纹理坐标可以采样深度图,然后将深度图中的数据逆推回模型空间就可以获得需要的场景深度。

1.顶点着色器中需要完成将顶点从模型空间转换到裁剪空间下的任务。

2.来到片元着色器中,经过Unity 渲染流程的处理,顶点输出的裁剪空间顶点(SV_POSITION 语义)坐标的xy 分量已经做了透视除法和屏幕映射处理转换到了屏幕空间当中,z 分量也做了透视除法并转换到(0,1)范围内,w 分量仍然是观察空间下的深度值。


获取顶点的深度值:

裁剪空间顶点坐标w 分量就是视角空间下的深度值,将其除以远裁面就是0-1 的深度值。

远裁剪面的距离可以通过内置的_ProjectionParams.z 变量来获取。 

或者比较傻的办法:

1.上面讲到片元着色器中,裁剪空间中的顶点(SV_POSITION 语义)坐标的z 分量已经做了透视除法并转换到了(0,1)范围中,此时只需要将其逆推回观察空间即可;

2.利用公式:(n表示远裁剪面;f表示近裁剪面;d表示ndc中重映射后的深度值,即第2步骤计算出的数值)

zv = 1/((n-f/n*f)*d + 1/n) 计算出观察空间下的深度值,将其除以远裁剪面就是0-1范围的观察空间下的深度值,公式:

z01=1/((n-f)/n*d + f/n)

以上公式实际上是将渲染管线处理后的顶点逆推回观察空间中,实际上是结合了投影矩阵z 分量上的处理公式、透视除法公式、屏幕映射公式三个步骤逆推出来的公式。

之所以说这个方法很傻,是因为绕了一个圈,因为顶点从裁剪空间转到屏幕空间下又逆推回了观察空间,之所以要介绍这种办法,是因为在遇到需要获取场景深度值的需求时,我们可以拿到的数据(即深度图)存储的正是步骤1 中顶点z 分量的值。

由于顶点在转换到裁剪空间时,其w 分量就是观察空间下的z 分量,如果只需要顶点的深度,还是建议直接使用裁剪空间顶点的w 分量作为深度值。

 

获取场景的深度值:

1.上面提到在片元着色器中,顶点坐标的xy 分量已经从裁剪空间转换到了屏幕空间。

2.直接将顶点坐标的xy 分量除以屏幕的长宽就可以得到0-1 范围的uv 坐标(屏幕纹理坐标),屏幕的长宽可以通过内置变量_ScreenParams 获取。

3.对深度图进行采样: SAMPLE_TEXTURE2D_X(_CamearaDepthTexture, sampler_CameraDepthTexture, uv)

采样深度图需要在shader 中作如下声明:

TEXTURE2D_X_FLOAT(_CameraDepthTexture);

SAMPLER(sampler_CameraDepthTexture);

4.利用公式:(n表示远裁剪面;f表示近裁剪面;d表示深度图中的数据,即第四步结果的x分量)

zv = 1/((n-f/n*f)*d + 1/n) 计算出模型空间下的深度值,将其除以远裁剪面就是0-1范围的深度值,公式:

z01=1/((n-f)/n*d + f/n)

也可以在顶点着色器中计算采样坐标,但是因为从顶点到片元着色器有一个插值过程,所以不能在顶点着色器中进行齐次除法,因此需要乘回w 分量,计算公式:(vc 为裁剪空间下的顶点坐标、vcw为该坐标的w分量)

vcw*(vc/vcw*0.5+0.5) => 0.5vc+0.5vcw

这也是内置函数ComputeScreenPos 的实现。

然后在片元着色器中进行齐次除法获得uv 坐标,然后从步骤3继续往下执行

如果在顶点着色器使用了ComputeScreenPos获得Vs;

那么对于步骤5可以使用unity 内部提供的两个方法Linear01Depth、LinearEyeDepth 传入屏幕纹理坐标和_ZBufferParams 来获取观察空间的场景深度和0-1线性深度,函数内部的原理即为上述步骤5。这也是shaderGraph 中SceneDepth 节点的做法。_ZBufferParams 是Unity 提供的内置变量,里面包含了远近裁剪平面相关的预计算。 同理,顶点的坐标也可以使用这种方式来获得,只不过顶点的深度我们可以直接通过齐次除法来取得。

源码

Shader "Custom/Depth"

{

Properties

{

}

SubShader

{

Tags { "RenderType"="Transparent" "Queue"="Geometry" "RenderPipeline" = "UniversalRenderPipeline"}

LOD 200

Pass

{

HLSLPROGRAM

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

#pragma vertex vert

#pragma fragment frag

struct a2v

{

float4 vertex : POSITION;

};

struct v2f

{

float4 vertex : SV_POSITION;

//结合ComputeScreenPos 使用

//float4 screenUV : TEXCOORD5;

};

CBUFFER_START(UnityPerMaterial)

CBUFFER_END

TEXTURE2D_X_FLOAT(_CameraDepthTexture);

SAMPLER(sampler_CameraDepthTexture);

v2f vert(a2v i)

{

v2f o;

o.vertex = TransformObjectToHClip(i.vertex.xyz);

//需要在片元中做透视除法

//o.screenUV = ComputeScreenPos(o.vertex,_ProjectionParams.x);

return o;

}

half4 frag(v2f i) : SV_TARGET

{

//顶点深度

half dv = i.vertex.w;

//顶点01 深度

half dv01 = i.vertex.w / _ProjectionParams.z;

//直接获取屏幕纹理坐标

float2 screenUV = i.vertex.xy / _ScreenParams.xy;

//结合ComputeScreenPos 计算屏幕纹理坐标

//float2 screenUV = i.screenUV.xy / i.screenUV.w;

//深度图中存储的深度,需要逆推回到观察空间

float dd = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r;

//观察空间中的0-1场景深度

float ds01 = Linear01Depth(dd, _ZBufferParams);

//观察空间中的场景深度

float ds = LinearEyeDepth(dd, _ZBufferParams);

return ds01;

}

ENDHLSL

}

}

}

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

推荐阅读更多精彩内容