【Unity ShaderLab】深度图详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/LeeXxs/article/details/81946598

目录

#一、深度图的获取

1.1 获取深度图

深度图是一张存储着高精度深度信息的渲染纹理,我们经常需要获取它存储的深度信息,来做一些后处理相关的效果;
深度图中的深度信息的范围值为[0,1],且为非线性分布
先不管它背后的原理,下面是获取深度图的步骤:

  • 设置摄像机的depthTextureMode,告诉摄像机你需要一张深度纹理,代码如下:
camera.depthTextureMode = DepthTextureMode.Depth;
  • 然后,我们就可以在shader中通过属性 _CameraDepthTexture 获取深度图:
sampler2D _CameraDepthTexture;	//深度图
  • 接着,我们可以使用当前像素纹理坐标,对深度图采样,这里可以使用 tex2D 采样,但有些平台上需要一些特殊处理(如PS3和PSP2),因此 Unity 提供了一个统一的宏 SAMPLE_DEPTH_TEXTURE,用来处理由于平台差异造成的问题:
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); //i.uv对应了当前像素的纹理坐标
  • 类似的宏还有SAMPLE_DEPTH_TEXTURE_PROJ 和SAMPLE_DEPTH_TEXTURE_LOD:
float depth2 = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screen); 
float depth3 = SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, UNITY_PROJ_COORD(float4(i.uv,0,0))); 

1.2 深度值线性化

  • 上面的 depth 就是深度值了,但是!这里获取的深度值 depth 是非线性的!所谓线性函数,从数学角度可以理解为一阶导数为常数的函数,从图像上线性函数看是一条直线。而这里的 depth 值和真正的深度值并不具有线性的函数关系,这可能影响到后续的计算,所以,有时候它并不是我们想要的那个数据,因此Unity 又提供了两个线性化的接口:
//得到的深度值范围:[Near,Far]	Near(近裁剪平面) Far(远裁剪平面)
float LinearDepth1 = LinearEyeDepth(depth);

//得到的深度值范围:[0,1]	
float LinearDepth2 = Linear01Depth(depth);

  • 很多时候,我们希望可以更加直观的查看深度值信息:
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv);
float LinearDepth = Linear01Depth(depth);
return fixed4(LinearDepth ,LinearDepth ,LinearDepth ,1.0);
  • 实际运行:
    这里写图片描述

在查看纹理的时候,会有画面全白或者全黑的问题。这是因为远近裁剪平面距离可能太大了,而相对的距离摄影机较近的物体会被映射到非常小范围内的深度值。这时候可以调整摄像机的远近裁剪平面,使摄影机的视椎体尽可能刚好覆盖所要渲染的物体。

虽然现在有深度信息了,后续可以该干啥干啥了( 运动模糊,景深 ,全局雾效…),但是遗留下来的问题也不少,比如说 Unity 是如何获取的深度值,深度值为什么会是非线性的,使用 LinearEyeDepth(depth) 和 Linear01Depth(depth) 的时候,Unity 又做了什么。这些都是接下来要讨论的问题。


#二、深度值详解
##2.1 Pipeline中的深度值
第一个问题,深度图中的数据是哪来的?
在顶点着色器流水线阶段,顶点着色器的最基本的功能,就是把模型顶点从模型空间转换到齐次裁剪坐标空间中。然后可以进行裁剪工作,当所有裁剪工作完成后,再进行投影操作(理解为空间的降维),即将视锥体投影到屏幕空间。各个阶段如下:

  • 模型空间—(模型变换M)—世界空间—(观察变换V)—观察空间—(透视投影矩阵变换P)—裁剪空间—(…裁剪…)—(齐次除法)—NDC—(映射输出)—屏幕空间

观察空间下的z值,经过了透视投影矩阵变换齐次除法的计算,处于NDC坐标系下后,又经过了一些处理(映射处理),才存储到了深度图中。不太准确的说,深度图中的数据是来自NDC下的z值。
NDC

##2.2 为什么深度值是非线性的?
深度值是非线性的,和投影矩阵变换齐次除法有关;

  • 观察空间—(透视投影矩阵变换)—裁剪空间

在这一步中,整个世界场景位于以摄像机为原点的观察空间坐标系下。因为只有位于*视锥体*内的物体才会被摄像机渲染并且可见,视锥体是一个金字塔形状,要直接使用6个锥平面进行裁剪比较麻烦;
因此,有一种更加通用方便的方法是:通过一个投影矩阵把顶点转换到一个裁剪空间中。经过投影矩阵缩放后,,我们可以直接使用w分量作为一个范围值,如果x,y,z分量都位于这个范围内,就说明该顶点位于裁剪空间内
以下是推导出来透视投影矩阵( 这里不做推导过程 ):
这里写图片描述
Near:近裁剪平面距离
Far:远裁剪平面距离
FOV:锥体竖直方向的张开角度
Aspect:摄像机的横纵比

需要注意的是,这里的观察空间是右手坐标系,即+x轴指向右方,+y轴指向上方,-z轴指向的是摄像机的正前方。这是Unity规定的,这种规定是符合OpenGL传统的(模型空间和世界空间是左手坐标系,观察空间是右手坐标系),所以使用列矩阵要在矩阵右侧进行相乘,且相乘变换后z分量范围将在 [-w,w] 之间。

下面是一个顶点和投影矩阵相乘后的坐标点:
这里写图片描述
从结果看,这个投影矩阵的本质是对 x,y,z 分量进行不同程度的缩放(z 分量还做了个平移),而缩放的目的自然是为了方便裁剪。此时的顶点的w分量已经不再是1,而是 z 分量的取反结果

  • 裁剪空间—(…裁剪…)
    现在,我们就可以通过不等式来判断一个变换后的顶点是否位于视锥体内,若是,则它变换后的坐标必须满足:

    当完成所有的裁剪工作后,就要进行真正的投影了,即把视锥体投影屏幕空间中,从而得到真正的二维像素坐标。

  • (…裁剪…)—(齐次除法)—NDC—(映射输出)—屏幕空间
    投影的过程分为两步:

    1. 齐次除法
      齐次除法也被称为透视除法,即用齐次坐标系的 w 分量去除以 x,y,z 分量。
      在上一步操作中,我们得到了裁剪空间下的 z 和 w 分量:

      然后,我们通过齐次除法就可以得到 NDC 下的 z 分量:

      经过这个步骤,得到的坐标称为归一化的设备坐标( Normalized Device Coordinates, NDC )。
      NDC坐标系中的z值范围为:[-1,1]
    2. 映射输出
      NDC下x,y的坐标范围也是[-1,1],而屏幕空间左下角像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight),因此x和y会先除以2再加1/2,映射到[0,1],然后再分别乘以pixelWidth和pixelHeight:

      上面的式子对x,y值做了处理,而z分量会直接被用于深度缓冲(Zbuffer),当然这个也不一定,会根据硬件选取合适的存储格式。
      除此之外,为了能将z值存储到深度图中,需要将其范围映射到 [0,1]:

      到这里就真相大白了,d 值就是深度图存储的数据。
      而d是非线性的,是因为d是关于z_ndc的函数,z_ndc是关于1/z_visw的函数,自然不是线性的了。

##2.2 Linear01Depth和LinearDepth函数解析
那么为了获得线性的深度值,可以根据d值,反推出z_visw的值:
这里写图片描述
而又因为z_visw在观察系坐标下,z轴坐标的正方向指向摄像机的正后方,所以还要取反,得到:

此时z_visw的取值范围就是视锥体深度范围,即[Near,Far]。如果我们想要得到范围在[0,1]之间的深度值,只需要把上面得到的结果除以Far即可。这样,0就表示该点与摄像机位于同一位置,1表示该点位于视锥体的远裁剪平面上。结果如下:

在第一章中,提到了使用如下函数能得到线性化的深度值:

//得到的深度值范围:[Near,Far]	Near(近裁剪平面) Far(远裁剪平面)
float LinearDepth1 = LinearEyeDepth(depth);

//得到的深度值范围:[0,1]	
float LinearDepth2 = Linear01Depth(depth);

看一下它们的函数定义:

// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}

// Z buffer to linear 0..1 depth (0 at eye, 1 at far plane)
inline float Linear01Depth( float z )
{
	return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}

下面是_ZBufferParams的定义:

uniform float4 _ZBufferParams;//用于线性化z buffer
//_ZBufferParams.x = 1-far/near
//_ZBufferParams.y = far/near
//_ZBufferParams.z = x/far
//_ZBufferParams.w = y/far

所以说LinearEyeDepth和Linear01Depth所做的工作分别如下:

  • LinearEyeDepth:返回一个范围为 [Near,Far] 的观察空间下的线性深度值
  • Linear01Depth:返回一个范围为 [ 0,1] 的观察空间下的线性深度值

综上,LinearEyeDepth和Linear01Depth负责把深度纹理的采样结果转换到视角空间下的深度值。

猜你喜欢

转载自blog.csdn.net/LeeXxs/article/details/81946598