1. 简介
屏幕后处理,指的是在渲染完整个场景得到屏幕图像后, 再对这个图像进行一系列操作,来实现各种屏幕特效。比如说景深效果的实现(离摄像机/焦距越远的部分,图片越模糊),这时候我们需要获取屏幕图像上每个像素点对应的物体相对于摄像机的距离,即深度。在Unity中,我们可以通过“深度图”(一张存储着高精度深度信息的渲染纹理)来获取深度信息。
2. 获取深度图
下面是获取深度图的步骤:
设置摄像机的depthTextureMode,告诉摄像机你需要一张深度纹理:
Camera.main.depthTextureMode = DepthTextureMode.Depth;
sampler2D _CameraDepthTexture;
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); //i.uv对应了当前像素的纹理坐标
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)中说起。在顶点着色器流水线阶段,顶点着色器的最基本的功能,就是把模型顶点从模型空间转换到齐次裁剪坐标空间中。然后进行裁剪工作,当所有裁剪工作完成后,再进行投影操作(理解为空间的降维),即将视锥体投影到屏幕空间。各个阶段如下:
模型变换
观察变换
透视投影矩阵变换
裁剪,齐次除法
映射输出
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所做的工作分别如下: