首页 > 渲染 > Unity深度和深度贴图

Unity深度和深度贴图

2019年8月25日
目录
[隐藏]

1. Depth计算方法

为了方便的获取场景的深度图,unity提供了一个便捷的方法,设置Camera. depthTextureMode |= DepthTextureMode.Depth; 这样Camera在渲染时会增加一个前置pass,将所有的opaque物体渲染一次,将深度保存到内置的一张深度图中。在渲染物体时,shader内通过sampler2D _CameraDepthTexture可以访问这张深度贴图。

通常投影并进行透视除法(以及OpenGL视口变换时会进行glDepthRange(0,1))之后,Direct3D和OpenGL的深度范围是[0,1],但是Unity做了一个修改叫做reversed Z ,在Direct3D平台下对齐次坐标进行透视除法后结果是[1,0]范围(不是很清楚unity为什么这样构建透视投影矩阵)。

可以看一下论坛里bgolus的说法 https://forum.unity.com/threads/decodedepthnormal-linear01depth-lineareyedepth-explanations.608452/

_CameraDepthTexture在不同平台有不同的存储方式,用UNITY_REVERSED_Z宏能够判断,如果是1,通常是Direct3D平台,深度范围是[1, 0];否则是OpenGL平台,深度范围是[0,1]。

透视投影时,如果是DX平台,深度的范围是[0,1],near plane为1,far plane为0,且不是线性关系,而是双曲线的右半部分,即

Unity在Direct3D平台下的深度曲线,经过投影和透视除法

而切换到OpenGL平台下,_CameraDepthTexture中保存的规则变为负半轴的双曲线,即

Unity在OpenGL平台下的深度曲线,经过投影、透视除法和glDepthRange(0,1)

越接近near plane,depth越接近0,越接近far plane,越接近1。

总结一下

  • Direct3D平台下,投影矩阵之后z∈[w, 0] -> 透视除法后z∈[1, 0]
  • OpenGL平台下,投影矩阵之后z∈[-w, w] -> 透视除法后z∈[-1,1] -> glDepthRange(0,1)后z∈[0, 1]

(2020.07.04)旧版本Unity4透视除法后D3D范围是[0,1],表现在GL.GetGPUProjectionMatrix 的差异。

(2020.07.04)不同渲染平台下投影矩阵不同。 Camera.projectionMatrix保存的是OpenGL规范的投影矩阵(无论哪个平台),传到shader前要调用GL.GetGPUProjectionMatrix()处理平台差异。

虽然两者使用了双曲线的不同段,但是共同点在于越接近near plane,获得的精度都越高,越远离near plane,精度越低,两者思路是一致的,通过减小f-n都能提高精度。

Clip space coordinates

Similar to Texture coordinates, the clip space coordinates (also known as post-projection space coordinates) differ between Direct3D-like and OpenGL-like platforms:

  • Direct3D-like: The clip space depth goes from +1.0 at the near plane to 0 at the far plane. This applies to Direct3D, Metal and consoles.
  • OpenGL-like: The clip space depth goes from –1.0 at the near plane to +1.0 at the far plane. This applies to OpenGL and OpenGL ES.

按照Unity文档的说法,透视矩阵投影并进行透视除法后,direct3d平台的深度在[1,0]范围内,opengl平台的深度在[-1,1]范围内。Opengl自己默认会调用glDepthRange(0,1)将透视除法得到的[-1,1]的深度映射到了[0,1]范围,这样两个平台都存储了[0,1]或者[1,0]范围内的非线性深度到贴图中。

对深度图采样时,Unity为了统一这两个平台,使得同样的深度值运算可以同时应用在两个平台,提供了两个内置函数,Linear01Depth和LinearEyeDepth将两个平台采样出来的深度都变换为近截面为0,远截面为1的线性值。根据Aras在unity论坛提供的说法,这两个函数的定义是

(2020.01.16) 这里后来发现Unity4.3中D3D平台下投影的结果也是[0,1]而不是[1,0],所以4.3里两个平台的算法统一了。而后面的版本_ZBufferParams在两个平台出现了差异,D3D平台投影之后范围是[1,0]。Aras在论坛里的留言是2010年的,那时候unity还是4版本,新版本unity算法不一样。下面都是之前针对4.3版本的解释

Unity代码这里注释中疑惑OpenGL的深度为什么也返回的是[0,1]区间,所以也用DX的计算方法。Aras在论坛里也提了相同的疑惑:

In OpenGL, the "depth" texture is much like a depth bufffer, i.e. it has non-linear range. Usually depth buffer range in OpenGL is -1...1 range, so in Humus' text, OpenGL math would have to be used. However, OpenGL depth textures seem to actually have 0..1 range, i.e. just like depth buffer in D3D. It's not explicitly written in the specification, but I found that out by trial and error. So since it matches D3D's depth buffer range, the D3D math has to be used.

我感觉Aras应该是没有考虑到OpenGL在透视除法之后的视口变换阶段不光会把x,y从NDC映射到[0,w-1]和[0,h-1]区间,也会自动把z映射到glDepthRange指定的depth区间,默认情况下是[0,1]区间。所以实际上OpenGL和DX一样,都需要先将[0,1]区间的映射值变换到NDC的[-1, 1]区间,然后再根据反函数计算camera空间的z值并进一步规范化到[0,1]的线性值。

以OpenGL平台为例,已知OpenGL的透视矩阵

深度depth(即透视除法后NDC里的z值)= (f+n)/(f-n) + 2*f*n/((f-n)*z), depth∈[-1, 1]

这个值范围在[-1,1]之间,随后经过视口变换,[-1,1]的深度被映射到[0,1]区间(由glDepthRange指定,默认为glDepthRange(0,1)),得到上面的双曲线,这个映射的值被写入Camera的DepthTexture作为深度贴图。

在shader里通过UNITY_SAMPLE_DEPTH或者SAMPLE_DEPTH_TEXTURE采样,得到的深度是一个非线性值,而且如上所说在不同平台深度规则不同。这时候需要使用Linear01Depth将这个值变换为线性值,具体做法是

  1. 将采样得到的深度从屏幕空间的[0,1]区间变换到NDC的[-1, 1]区间
  2. 将NDC区间的值通过上面提到的 depth= (f+n)/(f-n) + 2*f*n/((f-n)*z) 的反函数,求得摄像机空间中的z值,这个值得范围在[-f,-n]区间
  3. 对这个值取绝对值,落在[n,f]区间
  4. 如果是Linear01Depth函数,除以f,将结果落在[n/f, 1]区间,由于n,f均是正数,这个区间是[0,1]区间的子集;如果是LinearEyeDepth,不需要除f即得到结果

按照以上过程可以得到和unity一样的结果。通过变换,将非线性的深度值还原到摄像机空间中点的z值,并进一步normalize到[0,1]区间,点击查看推导的过程

从公式能看出来,Linear01Depth = LinearEyeDepth / far,如果far plane距离原点太远的话,会浪费过多精度。

  1. 深度图如果偏,意味着大部分深度都落在靠近far的部分,应该增加near 或 far
  2. 深度图如果偏,意味着大部分深度都落在靠近near的部分,应该减小near 或 far

其他参考:

  1. http://www.humus.name/temp/Linearize%20depth.txt

2. 渲染场景并保存深度

1) 保存到CameraDepthTexture

要想保存场景的深度,并给加下来的passes使用,做法是提供一个公用的材质,这个材质只需要mesh的顶点数据即可,输出深度值并保存。Unity Camera内置了一个Depth Texture,如果开启了摄像机的Depth渲染,就会增加一个前置过程,将场景中的opaque物体深度写入这张图中。

Camera Depth Texture并不是深度缓冲中的数据,而是通过ShadowCaster Pass获得,这样是为了给用户定制自己深度值映射的灵活性。默认是[0,1]范围内的非线性(双曲线)值(和深度缓冲中的一致)。比如通过shader replacement可以指定自定义的Shadow caster pass,将[0,1]的非线性值映射到线性值保存。

用FrameDebugger查看可以发现,在Drawing之前会增加一个UpdateDepthTexture过程

在接下来的Drawing中,可以通过sampler2D _CameraDepthTexture访问这张深度图。

2) 读取深度图\获得片段的深度

Camera的depth texture使用了shader replacement方法来渲染深度,只要是具有LightMode="ShadowCaster"的pass就会被渲染到_CameraDepthTexture上,否则不会渲染。渲染物体时,使用一个公共的深度渲染材质。

如果要定制自己的深度图,也要使用这种方式,下面是例子

这个shader从_CameraDepthTexture中采样,并实现了Linear01Depth和LinearEyeDepth,以及根据NDC z坐标计算片段的线性depth。两种方式得到的结果是一样的。

如果要获得此片段的深度,在fragment shader中用NDC坐标的z值,需要对平台进行判断,如果是Opengl平台的话,需要自行将[-1,1]范围映射到[0,1],即

depth01 = (depth + 1) / 2

然后在用Linear01Depth或者LinearEyeDepth将非线性的深度解码成线性的。

下面解释一下相关宏和函数的实现

float4 ComputeScreenPos(float4 clip_pos)

这个函数用来计算线性的屏幕坐标(实际上名不副实,其实是将[-w,w]范围内的齐次坐标xy缩放到[0, w]范围),并保留zw值不变。这里的齐次坐标是不能做透视除法的,因为重心插值只能对线性变量插值,做了除法以后就不是线性值了,要保证插值永远在线性空间内进行(物体、世界、相机、齐次四个空间)。

为什么不能在顶点着色器进行透视除法

inline float4 ComputeNonStereoScreenPos(float4 pos) {
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w; //_ProjectionParams.x是1或-1
o.zw = pos.zw;
return o;
}

inline float4 ComputeScreenPos(float4 pos) {
float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
return o;
}

在fragment shader中,对插值得到的此片段的屏幕[0,w]坐标进行采样。

UNITY_PROJ_COORD

处理平台差异,PSP2以外的平台可以无视。

half4 tex2Dproj(sampler2D s, in half4 t)

half4 tex2Dproj(sampler2D s, in half4 t)
{
return tex2D(s, t.xy / t.w);
}

这个函数对插值得到的屏幕0w坐标进行透视除法得到01UV坐标并采样。

UNITY_SAMPLE_DEPTH(color)

#define UNITY_SAMPLE_DEPTH(value) (value).r

返回深度图对应component的值

SAMPLE_DEPTH_TEXTURE_PROJ(sampler2D tex, half4 uv) 

#define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)

tex2Dproj和UNITY_SAMPLE_DEPTH的组合

SAMPLE_DEPTH_TEXTURE(sampler2D tex, half2 uv) 

#define SAMPLE_DEPTH_TEXTURE(sampler, uv) (tex2D(sampler, uv).r)

通常用在后处理中读取深度图,这时候uv代表屏幕空间的01坐标。

Linear01Depth和LinearEyeDepth用来将非线性的NDC空间深度重建为相机空间的线性深度

Linear01Depth(float depth)

将非线性的深度变换为[0,1]范围内的线性深度,计算方法在前面

LinearEyeDepth(float depth)

和Linear01Depth的差别是这个在[0,far]范围

这两个函数处理了direct3d和opengl平台差异性,将direct3d的[1,0]非线性空间和opengl的[0,1]非线性空间都计算到了[0,1]的线性空间。

下面自己实现一下这两个函数

direct3d平台近平面是1,远平面是0,需要翻转一下。

总结

SAMPLE_DEPTH_TEXTURE(sampler2D tex, float2 uv)  通常用在后处理中

SAMPLE_DEPTH_TEXTURE_PROJ(sampler2D tex, float3 clip) 和ComputeScreenPos配合使用

SAMPLE_DEPTH_TEXTURE_LOD

Linear01Depth(float z)

LInearEyeDepth(float z)

_CameraDepthTexture

_CameraDepthNormalsTexture

ComputeScreenPos

3. 深度图的应用

https://www.jianshu.com/p/80a932d1f11e

https://zhuanlan.zhihu.com/p/27547127?refer=chenjiadong

https://zhuanlan.zhihu.com/p/92315967

分类: 渲染 标签: