关于 Sahder 中的深度图,其应用方式有很多,最常见的例如 科幻游戏中扫描线、水的 Shader、透明物体相交、屏幕后期处理的描边等。
主要就是因为在能够获得场景深度值之后,可以有许多操作空间。
# 前言
这篇文章,其实在之前整理『Unity3DSahder 关键字』及『Sahder 内置函数』的时候就在想写一下了。
不过在那之后,有几个原因导致一直没有动手:
- 感觉只是个小东西,复杂之处在于用它来实现的各种后期效果;
- 虽然知道原理,但自己动手搞了搞之后,感觉理解起来有点困惑,这样就更不好说写什么了;
- 网上相关文章也挺多;
另外也有点其他事情耽搁了。
不过后面考虑了下,不写写加深理解,总结实践下,那后面再遗忘可就真没收获了。
因此在这里,我会简单总结一下深度图分别在屏幕后期处理及单个物体 Shader 中的用法及作用。
# 获取深度图
在获取深度值方面,主要有两种方式:
- 通过获取整个屏幕深度图
- 在单个物体特殊应用时,获取自身深度值
在 Unity 中,引擎是直接提供了屏幕深度图的,使用方式也很简单:在 Shader 中声明 sampler2D _CameraDepthTexture;
变量即可,运行时 Unity 会自动为其赋值。
然后通过纹理采样函数,即可获取到指定 屏幕坐标 的深度值信息。
注意采样到的深度值,并非线性 [0,1] 深度值,可使用 Unity 提供的函数 Linear01Depth 转化。
注:仅 “不透明” 对象(这些对象的材质和着色器设置为使用小于等于 2500 的渲染队列)会自动被渲染到深度纹理中
另外在这里还需要注意两点:屏幕后期处理 Shader 与物体自身 Sahder 采样的差异。
# 屏幕后处理对深度图采样
屏幕后期处理 Sahder 采样方式比较简单,直接将 i.uv 传入采样函数,返回的即是屏幕对应位置的深度值。
片元处理函数:
float depth = Linear01Depth(tex2D(_CameraDepthTexture, i.uv).x);
# 物体对屏幕深度值采样
不过若使用者是某个物体的 Sahder,那么就不能直接通过 uv 去采用深度了;这也是理所当然,物体 uv 跟屏幕坐标可没有半分关系。
因此需要计算出顶点所在的屏幕空间坐标,然后将屏幕空间坐标当做 uv 对深度图进行采样。
Unity 同样提供了对应函数 ComputeScreenPos(x)
,其中 x 是投影空间坐标。
在顶点处理函数传入片元数据结构体中,定义一个四维变量,调用该方法即可获取屏幕坐标。
o.screenPos=ComputeScreenPos(o.vertex);
然后在片元处理函数中,进行纹理采样。
注意此处是使用 tex2Dproj 采样。
float sceneZ = tex2Dproj(_CameraDepthTexture,i.screenPos).x;
或者使用 tex2D,手动除以 w 分量亦可:
(注:原因是 uv 采样值范围是 [0,1],因此计算出来的屏幕坐标需除以 w 分量映射至 [0,1] 区间)
float sceneZ = tex2D(_CameraDepthTexture,i.screenPos.xy/i.screenPos.w).x;
直接将采样的深度值作为颜色输出效果:
(物体采样 (左) 后处理屏幕采样 (右))
# 屏幕后处理对深度值的应用
关于屏幕后处理对深度图的应用,最简单的,大约要数 “扫描线” 这种了。
单纯地对比当前深度值及距离,就可以做出一个效果,网络上大多数关于深度图的文章,似乎也是第一个就在讲这个。
因此,秉着从简单开始原则,我也来一份。
首先,为了简单起见,这里就不使用其它什么额外效果,直接定义两个所需的基础变量:
- _Depth 作为当前深度距离
- _Width 作为扫描线的宽度
_Width("Width",Float) = 0.1
_Depth("Depth",Range(0,1)) = 0.1
然后,在片元处理函数中,分别采样屏幕原图和深度图。
这时候就可以通过 _Depth 参数 和深度图中的数值进行对比,计算结果。
判断标准是什么?
- 深度值小于 _Depth
- 深度值大于 _Depth-_Width 形成宽度
在这个区间内,可以形成一条宽度 _Width 的横线
如下代码所示:
//采样屏幕原图
float4 col = tex2D(_MainTex, i.uv);
//采样深度图
float depth = Linear01Depth(tex2D(_CameraDepthTexture, i.uv).x);
float distance = _Depth;
//判断深度值是否处于当前设置的区间内
if (depth < distance && depth> distance - _Width) {
return float4(1, 0, 0, 1)+col;
}
return col;
(最基础的效果)
在这块代码中,判断当深度值处于当前设置区间内,则直接将红色与当前颜色融合,否则返回当前颜色。很简单的一个逻辑。
如果觉得这条横线的边太硬,想稍微加点效果的话,还可以根据当前颜色深度所处宽度比例,作边缘柔化处理:
float diff = 1 - (distance - depth) / _Width;
return float4(1, 0, 0, 1)*diff+col;
由于上面是深度位于 distance - _Width 区间才会进来,所以 distance - depth 取值范围为 [0,_Width]。
因此 distance - depth 便是当前所处区间的位置,除以宽度,得出当前所处位置的比例。根据比例逐步提高线条颜色值,可以形成一个简易的柔化效果。
(加柔边效果)
屏幕后期处理中,上述只是最简单和基础的一个应用,深度图的作用还有做屏幕描边啊、模糊,做雾效等等,在此就不多说了。
# 物体 Shader 对深度值的应用
在物体 Sahder 中,如果需要获取深度图,那么一般来说,自身深度都是不写入深度图的,ZWrite 通常为 ZWrite Off
。
因此主要有两个操作:
- 计算自身深度值
- 采样自身位置原本深度值
然后通过两者相差,进行深度比较执行其它逻辑。
最常见的例如水的 Shader,在靠近岸边的时候波浪、渐变效果,物体相交检测等。应用方面,这里主要就说一下物体相交检测的效果。
为了计算自身深度,可以直接使用 Unity 提供函数 COMPUTE_EYEDEPTH (x),需要在顶点处理函数中调用,返回当前顶点深度值。
为了采样自身原本位置深度值,需要计算出当前顶点对应屏幕坐标,然后对深度图进行采样。
v2f:
float4 screenPos: TEXCOORD3;
vert:
o.screenPos = ComputeScreenPos(o.vertex);
COMPUTE_EYEDEPTH(o.screenPos.z);
由于计算出来的屏幕坐标,z 值实际上是没用的 (投影纹理查询只用到 xy/w),因此可以直接将自己的深度值存放至 screenPos 的 z 值中,传入片元处理函数。
来到片元处理函数后,采样深度图中的深度值,并将其与物体当前深度值进行对比,直接输出,可得到如下效果:
代码如下:
//场景深度
float sceneZ = LinearEyeDepth(tex2Dproj(_CameraDepthTexture,i.screenPos).x);
//自身深度
float selfZ = (i.screenPos.z);
//取得差值
float diff = (sceneZ-selfZ );
//输出差值
return float4(diff, diff, diff,1);
我们需要实现一个 相交描边 的效果,那么可以定义一个描边强度值 _OutLineValue("RimValue", Range(0,1)) = 0.1
大于该值输出描边颜色,否则可以输出黑色。
将直接返回深度差值修改为:
return float4(step(diff, _OutLineValue) , 0, 0, 0.5);
step 为 shder 内置函数,step (diff, _OutLineValue) 表示,当 diff 小于_OutLineValue 时,返回 1,diff 大于_OutLineValue 时,返回 0。
直接当做 R 通道颜色放进去,可以方便低实现:深度值低于_OutLineValue 显示红色描边 的需求。
如此,最简单的一个 相交描边 效果就实现了,代码很简单,所以效果也很简陋。
为了优化点显示的话,可以加一点其它效果:
//场景深度
float sceneZ = LinearEyeDepth(tex2Dproj(_CameraDepthTexture,i.screenPos).x);
//自身深度
float selfZ = (i.screenPos.z);
//取得差值
float diff = (sceneZ-selfZ);
//计算相交的描边强度
float outLineStrenth =1- saturate(diff / _OutLineValue);
//计算视线夹角,实现轮廓高亮
float rimStrenth=1-abs(dot(i.normal, i.viewDir))*_RimValue;
return float4(0,0,0,0.5)+float4(1,0,0,0)*max(outLineStrenth, rimStrenth);
因为这里的物体是透明且双面显示,因此需要处理法线与视线为负的情况,这里直接将点乘结果转化为绝对值的正数。
最终返回值为半透明黑色叠加相交描边或轮廓颜色:在这里取两者最大值,可以保证相交和轮廓因为视角原因叠在一块的时候,可以优先展示程度更深的一方,效果更好。
另外外边缘轮廓还可以直接使用菲涅尔效果展示 (比上面单纯视线和法线夹角好用,当然计算也稍微多一点)。
一般游戏中,使用的简化版菲涅尔公式为:
代码可修改为:
float rim=pow(1-abs(dot(i.normal, i.viewDir)), 5)*_RimValue;
当然,其中 5 这个定量也可以修改为变量,已进行更细致的效果调整。
(效果 直接轮廓高亮 (左) 菲涅尔 (右))
# 总结
以上便是深度图最简答的两个不同应用。
这次也是一边实践一边 Bing 一边写,花了两天多时间,总算收尾了。
怎么说呢,这一趟下来,还是有收获的。之前虽然知道大概怎么做,但实际理解起来还是有点糊的,因为自己不理解的也不敢直接写,所以从易到难一遍想一边慢慢再实现一遍,至少很多之前不太理解的地方也算想通了。
后边除非许久不再研究这一块,就算忘了,看看自己写的文章,应该也能很快拾起来吧。