【Unity ShaderLab】屏幕后处理:深度图浅析

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

内容

  1. 简介  
  2. 获取深度图
  3. 线性化深度值
  4. 深度图浅析

1. 简介

屏幕后处理,指的是在渲染完整个场景得到屏幕图像后, 再对这个图像进行一系列操作,来实现各种屏幕特效。比如说景深效果的实现(离摄像机/焦距越远的部分,图片越模糊),这时候我们需要获取屏幕图像上每个像素点对应的物体相对于摄像机的距离,即深度。在Unity中,我们可以通过“深度图”(一张存储着高精度深度信息的渲染纹理)来获取深度信息。

 

2. 获取深度图

下面是获取深度图的步骤:

  • 设置摄像机的depthTextureMode,告诉摄像机你需要一张深度纹理:

Camera.main.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)));

3. 线性化深度值

上面采样后获取的 depth 就是深度值了,But!这里获取的深度值大多情况下是不能直接使用的!还需要经过一步叫做“线性化”的操作,暂时不需要知道它是怎么实现的,这里Unity提供了两个线性化的接口,代码如下:

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

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

通过 LinearEyeDepth(depth) 可以得到一个范围为 [Near,Far] 的线性深度值;通过LinearEyeDepth(depth)可以得到一个范围为 [0,1] 的深度值。我可以通过在片元着色器中用如下方式输出,来观察验证得到的数据:

return fixed4(LinearDepth2 ,LinearDepth2 ,LinearDepth2 ,1.0);//LinearDepth2的范围为[0,1]

处理后的图片如下,可以看到离摄像机越远的地方越亮,即深度值越大。

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

 

有了深度值,后续可以该干啥干啥了( 运动模糊,景深 ,全局雾效......),但是仅仅是学会使用是无法满足人们群众日益增长的需求的!接下来,需要讨论如下几个问题:

  • “非线性”与“线性”指的是?

  • 深度图中的数据为什么是非线性的?

  •  LinearEyeDepth(depth) 和 Linear01Depth(depth) 做了什么?

 

4. 深度图浅析

4.1 非线性”与“线性

线性与非线性的一个明显区别是叠加性是否有效。在一个系统中,如果两个不同因素的组合作用只是两个因素单独作用的简单叠加,这种关系或特性就是线性的。反之,如果一个系统中一个微小的因素能够导致用它的幅值无法衡量的结果,这种关系或特性就是非线性的。相应地,具有叠加性的系统,是线性系统;反之,则属于非线性系统。

举个不准确的例子,在线性情况下,假设用0表示零深度,用1表示100个单位的深度值,那么就可以0.5表示50个单位的深度值,因为它们是线性相关的;但是,在非线性情况下,同样是上述的假设条件下,0.5就不一定是50个单位的深度值了。这就是为什么我们不能直接使用从深度图中采样出来的数据的原因,因为深度图存储的数据也是非线性的,这样的数据是无法用于计算的。


4.2 非线性的深度值

对于这样不单纯又做作的深度图,我的内心是拒绝的,就不能直接存储下范围为[0,1]的线性深度值么......于是我又查了一下资料,基本弄清楚了这一套流程。

首先,得从渲染管线(Pipeline)中说起。在顶点着色器流水线阶段,顶点着色器的最基本的功能,就是把模型顶点从模型空间转换到齐次裁剪坐标空间中。然后进行裁剪工作,当所有裁剪工作完成后,再进行投影操作(理解为空间的降维),即将视锥体投影到屏幕空间。各个阶段如下:

  • 模型空间

模型变换

  • 世界空间

观察变换

  • 观察空间

透视投影矩阵变换

  • 裁剪空间

裁剪,齐次除法

  • 标准化设备坐标(Normalized Device Coordinates, NDC)

映射输出

  • 屏幕空间

 

  1. 模型变换观察变换,使将物体的坐标从模型空间,通过旋转移动等复合变换,转换到了以摄像机为原点的观察空间坐标系下。在观察空间下,z轴上的数据表示了物体距离摄像机的远近,即深度,此时的深度值还是线性的。(观察空间使用的是右手坐标系,即 -z轴才是摄像机的正前方)
  2.  观察空间中,只有位于视锥体内的物体才会被摄像机渲染并且可见,所以需要将范围外的顶点剔除。但是视锥体是一个金字塔形状,由六个锥平面构成。使用六个锥平面直接进行裁剪判断会很麻烦; 因此,有一种更加方便的方法是:通过一个透视投影矩阵把顶点从观察空间转换到一个裁剪空间下,转换的过程实际是对x,y,z分量都进行了不同程度的缩放和平移。使x,y,z值满足:直接用w分量作为裁剪的范围值,如果变换后的x,y,z分量都位于[-w,w]这个范围内,就说明该顶点位于裁剪空间内,反之会被剔除。 以下是推导出的透视投影矩阵和顶点相乘后的坐标点(这里不做推导过程):
    Near:近裁剪平面距离
    Far:远裁剪平面距离
    FOV:锥体竖直方向的张开角度
    Aspect:摄像机的横纵比
  3. 在上一步操作中,我们可以得到裁剪空间下的 z 和 w 分量:

  4. 经过透视投影变换,坐标系转换到了裁剪空间下。紧接着,我们可以通过判断当前的 x,y,z 分量是否处于 [-w,w] 范围内,来判断坐标点是否处于视锥体内,并将视锥体外的坐标点剔除。在观察空间,世界空间,模型空间下,坐标点的w分量一直都是1,所以说,经过剔除后的裁剪空间下的坐标点的范围值为 [-1,1]
  5. 当完成所有的裁剪工作后,就要进行真正的投影了,即把视锥体投影到屏幕空间中,从而得到真正的二维像素坐标。 期间需要进行两步操作,齐次除法映射输出
  6. 齐次除法:用齐次坐标系的 w 分量去除以x,y,z分量。以z轴分量为例:

    经过齐次除法后,坐标系由裁剪空间转换到了NDC(归一化的设备坐标,Normalized Device Coordinates)下,此时的x,y,z分量的范围值为[-1,1]。
  7. 映射输出: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 值就是 Unity 中深度图存储的数据。因为d是关于z_ndc的函数,z_ndc是关于1/z_visw的函数,因此 d 自然不是线性的了。 (z_ndc:ndc空间下的z值;z_visw:观察空间下的z值)

4.3  Linear01Depth 和 LinearDepth 函数

那么为了获得线性的深度值,可以根据d值,反推出z_visw的值:

而又因为z_visw在观察系坐标下,z轴坐标的正方向指向摄像机的正后方,所以还要取反,得到:

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

在Unity中,提供了两个帮助我们获取线性化深度值的函数:LinearDepth 和 Linear01Depth;并且分别可以得到 [Near,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] 的观察空间下的线性深度值:

猜你喜欢

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