神奇的深度图:复杂的效果,不复杂的原理
0x00 前言
本文是《有趣的深度图》的第二篇文章,上一篇文章《有趣的深度图:可见性问题的解法》中已经和大家介绍了深度图在解决可见性问题中的应用。其实,利用深度信息我们可以实现很多有趣而又显得“高大上”的效果。
不过这些效果虽然看上去高大上,但是一旦了解了原理就会发现实现这种效果其实是十分简单的。
那么本文会包括以下四个有趣的效果在Unity中的实现:
为了利用深度信息来实现若干效果,我们首先需要获取场景的深度信息。在移动游戏开发中常用的前向渲染路径(Forward Rendering)下,我们需要手动设置相机,让它提供场景的深度信息。
camera.depthTextureMode = DepthTextureMode.Depth;如果在延迟渲染路径(Deferred Lighting)下,由于延迟渲染需要场景的深度信息和法线信息来做光照计算,所以并不需要我们手动设置相机。
这样我们就可以在shader中访问_CameraDepthTexture来获取保存的场景的深度信息,之后再利用UNITY_SAMPLE_DEPTH这个宏来处理_CameraDepthTexture的值,这样我们就获取了某个像素的深度值。
float depth = UNITY_SAMPLE_DEPTH(tex2D(_CameraDepthTexture, uv));但是正如上一篇文章中所说,此时的深度值并非是线性的,因此我们常常需要利用另一个内建的方法Linear01Depth将结果转化为线性的。这样,我们就能将场景的深度信息渲染为一张灰度图。
float linear01Depth = Linear01Depth(depth); 0x02 有点科幻的扫描网不知道有没有小伙伴玩过《无人深空》这款游戏,当初ps4版预售时我就用行动支持了这款看上去很有吸引力的沙盒游戏,当然第二天挂闲鱼就是后话了。虽然这款游戏让人感到有些失望,但是其中的一些画面效果还是很有趣的,而且也和这篇文章的主题相关——利用场景的深度信息来实现一些科幻效果——比如说,在星球上用扫描仪进行扫描的效果。
我们也可以在Unity中实现类似的效果,关键就是利用场景的深度信息。
因此如果项目使用了前向渲染路径,我们就必须在脚本中手动将相机的depthTextureMode 设置为DepthTextureMode.Depth,如果是延迟渲染则不需要我们手动设置。
camera.depthTextureMode = DepthTextureMode.Depth;其次,这种全屏效果常常作为屏幕特效(image effect)来实现,也就是说我们需要摄像机先将场景渲染成一副图片,之后对这张图片的像素做处理。设想一下如果不这样做的话,我们不仅要计算场景内所有被渲染对象和摄像机的距离,还需要至少两个pass,其中一个返回被渲染物体的正常颜色,另一个则来实现和扫描颜色的叠加。如果场景内被渲染的对象很多的话,这样的操作效率就变得十分低下了。
所以,在cs脚本中我们还会用到OnRenderImage这个回调以获取摄像机渲染的场景图像。
再次,随着时间的流逝扫描网逐渐扫描整个场景显然是一个动态的效果。因此我们还需要把时间这个因子也引入,时间影响了扫描网和起点的距离。当然,我们既可以在shader文件中考虑时间的影响,也能在cs文件中考虑时间的影响。
如果我们要直接在shader中获取时间的信息的话,就需要用到unity的内置变量float4 _Time : Time (t\/20, t, t*2, t*3)了。它的4个分量分别表示了t/20、t、t*2、t*3。因此,在shader中我们使用_Time.y就可以获取当前的时间了,根据时间我们就能算出扫描网当前移动的大概距离了。
除此之外,我们当然也可以在cs文件中直接利用Time类和Update方法直接计算扫描网的移动距离,然后再将结果传入shader。这样,我们就完成了一个超级简单的cs脚本:
/* * Created by Chenjd * */ using UnityEngine; using System.Collections; public class ScannerEffect : MonoBehaviour { #region 字段 public Material mat; public float velocity = 5; private bool isScanning; private float dis; #endregion #region unity 方法 void Start() { Camera.main.depthTextureMode = DepthTextureMode.Depth; } void Update() { if (this.isScanning) { this.dis += Time.deltaTime * this.velocity; } //无人深空中按c开启扫描 if (Input.GetKeyDown(KeyCode.C)) { this.isScanning = true; this.dis = 0; } } void OnRenderImage(RenderTexture src, RenderTexture dst) { mat.SetFloat("_ScanDistance", dis); Graphics.Blit(src, dst, mat); } #endregion }至于shader?那就更简单了,现在我们获取了相机渲染之后的场景图,这样图上的每个像素只需要获取自己的深度信息:
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth); float linear01Depth = Linear01Depth(depth);然后再和扫描网现在的位置做个对比——当然我们还可以加入扫描网的宽度这个概念——符合条件的像素颜色和扫描网的颜色进行叠加就可以了。最后为了更完美一点,我们还需要判断一下深度值是否比1小,因为深度值在[0,1]这个区间内,而1对应的是远裁切面,因此如果不判断1的话,整个远方最后都会被扫描网的颜色进行叠加。
if (linear01Depth < _ScanDistance && linear01Depth > _ScanDistance - _ScanWidth && linear01Depth < 1) { float diff = 1 - (_ScanDistance - linear01Depth) / (_ScanWidth); _ScanColor *= diff; return col + _ScanColor; }完整的项目可以到这里到这里下载:UnitySpecialEffectWithDepth
0x03 透过墙壁绘制背后的“人影”透过障碍物看到障碍物后的高亮目标,国内外很多游戏都会用到类似的效果。
这个看上去很有高大上的视觉效果,其实从创建一个unity的Unlit shader文件到最后完成这个效果只需要大概30s。