神奇的深度图:复杂的效果,不复杂的原理

0x00 前言

本文是《有趣的深度图》的第二篇文章,上一篇文章《有趣的深度图:可见性问题的解法》中已经和大家介绍了深度图在解决可见性问题中的应用。其实,利用深度信息我们可以实现很多有趣而又显得“高大上”的效果。
不过这些效果虽然看上去高大上,但是一旦了解了原理就会发现实现这种效果其实是十分简单的。
那么本文会包括以下四个有趣的效果在Unity中的实现:

  • 有点科幻的扫描网
  • 透过墙壁绘制背后的“人影”
  • 护盾/能量场效果
  • 边缘检测

0x01 获取深度信息

为了利用深度信息来实现若干效果,我们首先需要获取场景的深度信息。在移动游戏开发中常用的前向渲染路径(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);
QQ截图20170619232147.png

0x02 有点科幻的扫描网

不知道有没有小伙伴玩过《无人深空》这款游戏,当初ps4版预售时我就用行动支持了这款看上去很有吸引力的沙盒游戏,当然第二天挂闲鱼就是后话了。虽然这款游戏让人感到有些失望,但是其中的一些画面效果还是很有趣的,而且也和这篇文章的主题相关——利用场景的深度信息来实现一些科幻效果——比如说,在星球上用扫描仪进行扫描的效果。

nomansky.gif

我们也可以在Unity中实现类似的效果,关键就是利用场景的深度信息。

scaneffect.gif

因此如果项目使用了前向渲染路径,我们就必须在脚本中手动将相机的depthTextureMode 设置为DepthTextureMode.Depth,如果是延迟渲染则不需要我们手动设置。

camera.depthTextureMode = DepthTextureMode.Depth;

其次,这种全屏效果常常作为屏幕特效(image effect)来实现,也就是说我们需要摄像机先将场景渲染成一副图片,之后对这张图片的像素做处理。设想一下如果不这样做的话,我们不仅要计算场景内所有被渲染对象和摄像机的距离,还需要至少两个pass,其中一个返回被渲染物体的正常颜色,另一个则来实现和扫描颜色的叠加。如果场景内被渲染的对象很多的话,这样的操作效率就变得十分低下了。
所以,在cs脚本中我们还会用到OnRenderImage这个回调以获取摄像机渲染的场景图像。

void OnRenderImage(RenderTexture src, RenderTexture dst)
{
     //TODO
}

再次,随着时间的流逝扫描网逐渐扫描整个场景显然是一个动态的效果。因此我们还需要把时间这个因子也引入,时间影响了扫描网和起点的距离。当然,我们既可以在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
 * http://www.cnblogs.com/murongxiaopifu/
 */ 
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。

原理很简单,即根据目标是否被遮挡返回不同的颜色即可。目标被障碍物遮住的部分其深度值必然要大于障碍物,因此我们可以用一个pass处理当深度值大于障碍物的时也就是目标被障碍物遮住的部分的颜色——例如我们返回红色。

    Pass
    {
        ZTest Greater

        ...

        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 col = fixed4(1, 0, 0, 1);
            return col;
        }
    }

再用另一个pass处理目标未被遮挡住的部分,也就是深度值小于障碍物时返回目标的正常颜色。

    Pass
    {
        ZTest Less 

        ...

        fixed4 frag (v2f i) : SV_Target
        {
            fixed4 col = tex2D(_MainTex, i.uv);
            return col
        }
    }

不过墙后的敌人如果只是显示一个红色是否有点太单调了呢?还有很多游戏,它的透视效果是下面这样的:目标身上多了一些描边。


杀出重围3人类革命

这个效果的实现其实也很简单。我们可以根据观察方向和目标多边形的法线方向的夹角来判断目标的边缘——毕竟目标面向我们的面的法线和我们观察方向的夹角相对较小,而边缘部分的面的法线和我们的观察方向的夹角显然更大——这里的边缘判断用到了观察方向,下文我们还会聊聊跟观察方向无关的边缘检测。


QQ截图20170625232518.png

所以,给墙后的目标描边这件事就又变得十分简单了。我们只需要在处理被遮挡部分的那个pass中返回的红色变为与法线和观察方向的夹角相关的一个值就好了。
为了实现这个目标,我们首先要获取法线和观察方向的信息。

o.normal = UnityObjectToWorldNormal(v.normal);
o.viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex).xyz);

之后再计算法线和观察方向的夹角信息:

float NdotV = 1 - dot(i.normal, i.viewDir) ;

最后,只需要把这个值当作影响最后颜色输出的因素就好了。

return _EdgeColor * NdotV;

seethewall4.gif

完整的项目可以到这里到这里下载:UnitySpecialEffectWithDepth

0x04 护盾/能量场效果

Winston_overwatch_gamemela.jpg

很多科幻游戏也有这种能量场或者护盾的效果。例如暴雪的守望先锋中的猩猩温斯顿的屏障发射器、光环系列的圣堂防卫者的能量护盾甚至一些手游中也有类似的效果,比如网易的光明大陆。


maxresdefault.jpg

这个效果的实现和原理其实也并不复杂。简单的说可以分为以下这几个部分:

  • 半透明效果
  • 相交高亮,主要指能量场和别的物体相交的地方是高亮显示
  • 表面扭曲
  • 一个和观察方向相关的描边效果

首先我们要开启透明混合并指定渲染队列为透明。

SubShader
{
    ZWrite Off
    Cull Off
    Blend SrcAlpha OneMinusSrcAlpha

    Tags
    {
        "RenderType" = "Transparent"
        "Queue" = "Transparent"
    }

    ...
}

之后像上一个例子那样,根据观察方向绘制能量场的边缘。

//vert
o.normal = UnityObjectToWorldNormal(v.normal);

o.viewDir = normalize(UnityWorldSpaceViewDir(mul(unity_ObjectToWorld, v.vertex)));


//frag
float rim = 1 - abs(dot(i.normal, normalize(i.viewDir)));

这样,我们就得到了一个半透且带有描边效果球体,能量场已经初具雏形了。

unitytip3.gif

接下来,我们就要实现相交高亮的效果了。所谓的相交高亮指的是能量场和别的物体相交时,在相交处绘制出高亮效果。这时我们就要用到深度信息了。当能量场和某个物体相交时,二者的深度信息应该一致,基于这个对比深度信息,我们可以用来估计一个像素的“相交程度”。

需要注意的是,能量场的shader在执行时_CameraDepthTexture中只保存了场景中不透明物体的深度信息,因此这个时候无法从CameraDepthTexture中获取能量场的深度信息,所以要在vert中计算顶点的深度,这里我利用了COMPUTE_EYEDEPTH这个内置的宏。在之后的frag内就可以很方便的获取场景和能量场当前片元的深度了。

//vert
o.screenPos = ComputeScreenPos(o.vertex);
COMPUTE_EYEDEPTH(o.screenPos.z);


//frag
float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.screenPos)));
float partZ = i.screenPos.z;

两者相减就是深度的差异diff,再用1 - diff就得到了一个“相交程度”。

float diff = sceneZ - partZ;

float intersect = (1 - diff) * _IntersectPower;
unitytip4.gif

最后我们还需要实现一个能量场的扭曲效果。扭曲效果是游戏里面经常有的一个效果,其实也很简单,我们只需要一张渲染能量场之前的场景的渲染图,之后随时间调整uv的偏移就可以模拟扭曲的效果了。

GrabPass
{
    "_GrabTempTex"
}

...
  
//frag
float4 offset = tex2D(_NoiseTex, i.uv - _Time.xy) * _DistortTimeFactor;
i.grabPos.xy -= offset.xy * _DistortStrength;
fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);

...

unitytip901.gif

完整的项目可以到这里到这里下载:UnitySpecialEffectWithDepth

0x05 边缘检测

边缘检测的目的是标识数字图像中属性显著变化的点。图像属性中的显著变化通常反映了属性的重要变化。这些包括:

  • 深度上的不连续
  • 表面法线方向不连续
  • 颜色不连续
  • 亮度不连续
QQ截图20170623191104.png

需要注意的是边缘可能与观察方向有关——也就是说边缘可能随着观察方形的不同而变化,例如上文中的描边实现;也可能与观察方向无关——这通常反映被观察物体的属性如表面纹理和表面形状。在这个部分,我们的关注点主要是后者。

因此,根据不同的属性变化也有很多种策略来处理边缘检测,例如利用深度、利用法线、利用深度+法线、利用颜色等等。边缘是灰度值不连续的结果,这种不连续常可利用求导数方便地检测到,一般常用一阶和二阶导数来检测边缘。其中一阶导数的幅度值来检测边缘的存在,幅度峰值一般对应边缘位置。


11.png

不过为了简化计算,在实际中常用小区域模板卷积来近似计算偏导数。对Gx和Gy各用1个模板,所以需要2个模板组合起来以构成1个梯度算子。最简单的梯度算子是罗伯特交叉(Roberts cross)算子。

Roberts-Cross-convolution-filter.png

其实在unity的image effect中就包含了描边这个效果,而其中又有5种不同的方式,其中的一种叫做RobertsCrossDepthNormals便是利用了罗伯特算子,各位如果有兴趣的话可以参考。

0x06 小结

以上便是常见的几种利用深度信息来实现的视觉效果。
完整的项目可以到这里到这里下载:UnitySpecialEffectWithDepth
各位如果觉得有趣的话,欢迎点个赞。

-华丽的分割线-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》


欢迎大家关注我的公众号慕容的游戏编程:chenjd01

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • <转>我也忘了转自哪里,抱歉,感谢原作者 什么是Shader Shader(着色器)是一段能够针对3D对象进行操作...
    星易乾川阅读 5,584评论 1 16
  • 转载注明出处:点击打开链接 Shader(着色器)是一段能够针对3D对象进行操作、并被GPU所执行的程序。Shad...
    游戏开发小Y阅读 3,334评论 0 4
  • 午夜流霜飞落几更钟响 湖汀兰芷不复当年模样 月下的柳林 月照的湖心 不知湖上的月光等待何人 鸿雁长飞不度光年流淌 ...
    蓝色纸鹤1阅读 170评论 0 0
  • 老牛是咱家小区的一名园艺工。这些人多半不起眼。我之所以注意到他,是因为每天的某一时刻,我总能从家里的窗口看到他坐在...
    我可能用了假名字阅读 245评论 0 0
  • 第一次在深圳的秋天,来得刚刚好,国庆后就慢慢转凉。早晚一件薄外套,还好一瓶热好的牛奶,就是一天的开始。 喜欢英语,...
    奔向阳光的女孩阅读 728评论 0 1