创建一个夜视屏幕效果

我们的下一个屏幕效果绝对是一个更受欢迎的效果。在游戏市场中的 使命召唤现代战争(Call of Duty Modern Warfare)光环(Halo) 等第一人称游戏中都出现过。是使用一种独特的青柠色屏幕颜色,让整个图像增亮的效果。


为了获得这种夜视效果,我们需要使用Photoshop来剖析这个效果。它是这么一个简单的过程,在网上找一些相关的图片然后将这些图像组成一个层级,看看你将需要哪种混合模式,或者需要哪一种顺序来组合我们的这些层级。下图展示的是用Photoshop处理后的呈现效果:
diagram


让我们把由Photoshop组成的图像分解成不同的组成部分,好让我们可以更好的理解这些我们之后要组合的资源。在下一个知识点中,我们将涵盖这些的处理过程。


  • 始前准备
    开始制作我们这个屏幕效果,我们又要将这个效果分解成不同的组成层级。我们可以使用Photoshop创建一个效果图,这样对于创建我们的夜视效果来说,可以对如何构成这个效果提供一些灵感:

    • 绿色着色: 我们屏幕效果的第一层是标志性的绿色,几乎在每一张夜视图像中都能找到。这将给我们的效果带来标志性的夜视的样子,就像下图所示那样:
      diagram
    • 扫描线: 为了给玩家带来一种新的显示效果,提升效果呈现,我们在着色层的上面添加扫描线。为了这个,我们将用Photoshop创建一个纹理,让用户可以对纹理进行平铺,从而可以让这些扫描线变大或者缩小。
    • 噪音图: 我们的下一层级是一张简单的噪音纹理,我们将会把它平铺在平铺后的着色图像和扫描线的上面,然后将图像打散并且给我们的效果添加更多的细节。这一层简单的强调了 数字读出外观(digital read-out look) :[各位觉得听起来拗口,可以去查一下DRO的概念,我这里贴一个相关的百科]
      diagram
    • 渐晕纹理: 我们的夜视效果的最后一个层级是渐晕。如果看过使命召唤现代战争中的夜视效果,你就会注意到它使用了一个渐晕来仿造一个从单筒望眼镜看出来的效果。我们将在这个屏幕效果中也做同样的事情:
      diagram

    让我们聚集需要的这些纹理,开始创建我们的屏幕效果。请按照下面的步骤进行:

    • 1.收集一张渐晕纹理,一张噪音纹理和一张扫描线纹理,就跟我们上面看到哪些那样的就行。
    • 2.创建一个名为 NightVisionEffect.cs 的脚本和一个名为 NightVisionEffectShader.shader 的着色器。
    • 3.创建好这些代码文件后,给这些文件添加必要的代码好让屏幕效果系统可以设置好并且正常运行。至于该如何做的操作步骤,可以参考 第八章通过Unity渲染纹理实现屏幕效果

    最终,随着我们的屏幕效果系统设置好并且顺利运行,以及收集好所需的纹理后,我们就可以开始创建夜视效果的过程了。


  • 操作步骤
    当我们把所有的资源收集好并且让屏幕效果系统流畅的跑起来后,让我们开始给脚本和着色器添加一些必要的代码。我们将首先给 NightVisionEffect.cs 脚本添加代码,双击脚本在代码编辑器中打开这个文件。

    • 1.我们先要给脚本添加一些变量,这样可以让这个效果的用户在 检查器面板(Inspector) 中去调整这些变量。在 NightVisionEffect.cs 脚本中输入下面的代码:
    #region Variables
    public Shader nightVisionShader;
    
    public float constrast = 2.0f;
    public float brightness = 1.0f;
    public Color nightVisionColor = Color.white;
    
    public Texture2D vignetteTexture;
    
    public Texture2D scanLineTexture;
    public float scanLineTileAmount = 4.0f;
    
    public Texture2D nightVisionNoise;
    public float noiseXSpeed = 100.0f;
    public float noiseYSpeed = 100.0f;
    
    public float distortion = 0.2f;
    public float scale = 0.8f;
    
    private Material curMaterial;
    private float randomValue;
    #endregion
    
    • 2.接下来,我们需要完成 OnRenderImage() 方法,让我们可以传递正确的数据给着色器,这样着色器才能正确的处理屏幕效果。通过下面的代码来完成 OnRenderImage() 方法调整:
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if(nightVisionShader != null)
        {	
            material.SetFloat("_Contrast", constrast);
            material.SetFloat("_Brightness", brightness);
            material.SetColor("_NightVisionColor", nightVisionColor);
            material.SetFloat("_RandomValue", randomValue);
            material.SetFloat("_distortion", distortion);
            material.SetFloat("_scale",scale);
    
            if(vignetteTexture)
            {
                material.SetTexture("_VignetteTex", vignetteTexture);
            }
    
            if(scanLineTexture)
            {
                material.SetTexture("_ScratchesTex", scanLineTexture);
                material.SetFloat("_ScanLineTileAmount", scanLineTileAmount);
            }
    
            if(nightVisionNoise)
            {
                material.SetTexture("_NoiseTex", nightVisionNoise);
                material.SetFloat("_NoiseXSpeed", noiseXSpeed);
                material.SetFloat("_NoiseYSpeed", noiseYSpeed);
            }
    
            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
    
    • 3.为了完成 NightVisionEffect.cs 脚本,我们只需要确保这些具体的变量被限制在一个合理的范围。这个范围是随意的可以在之后修改。这些是可以工作的很好的一些值:
    private void Update()
    {
        constrast = Mathf.Clamp(constrast, 0f, 4f);
        brightness = Mathf.Clamp(brightness, 0f, 2f);
        randomValue = Random.Range(-1f, 1f);
        distortion = Mathf.Clamp(distortion, -1f, 1f);
        scale = Mathf.Clamp(scale, 0f, 3f);
    }
    
    • 4.现在我们能把注意力转移到屏幕效果的着色器部分上来了。如果你还没有打开着色器代码,用代码编辑器打开着色器,然后再着色器的 属性块(Properties block) 中输入下面的代码:
    Properties
    {
        MainTex ("Base (RGB)", 2D) = "white" {}
        _VignetteTex ("Vignette Texture", 2D) = "white"{}
        _ScanLineTex ("Scan Line Texture", 2D) = "white"{}
        _NoiseTex ("Noise Texture", 2D) = "white"{}
        _NoiseXSpeed ("Noise X Speed", Float) = 100.0
        _NoiseYSpeed ("Noise Y Speed", Float) = 100.0
        _ScanLineTileAmount ("Scan Line Tile Amount", Float) = 4.0
        _NightVisionColor ("Night Vision Color", Color) = (1,1,1,1)
        _Contrast ("Contrast", Range(0,4)) = 2
        _Brightness ("Brightness", Range(0,2)) = 1
        _RandomValue ("Random Value", Float) = 0
        _distortion ("Distortion", Float) = 0.2
        _scale ("Scale (Zoom)", Float) = 0.8
    }
    
    • 5.为了确保数据可以从 属性块(Properties block) 传递给 CGPROGRAM代码块(CGPROGRAM block),我们需要在CGPROGRAM代码块声明与属性块中相同名字的变量:
    CGPROGRAM
    #pragma vertex vert_img
    #pragma fragment frag
    #pragma fragmentoption ARB_precision_hint_fastest
    #include "UnityCG.cginc"
    
    uniform sampler2D _MainTex;
    uniform sampler2D _VignetteTex;
    uniform sampler2D _ScanLineTex;
    uniform sampler2D _NoiseTex;
    fixed4 _NightVisionColor;
    fixed _Contrast;
    fixed _ScanLineTileAmount;
    fixed _Brightness;
    fixed _RandomValue;
    fixed _NoiseXSpeed;
    fixed _NoiseYSpeed;
    fixed _distortion;
    fixed _scale;
    
    • 6.我们的效果还包括一个透镜畸变,用来进一步的表达透过镜头来观察的效果,并且图像的边缘由于镜头的角度而发生了扭曲。在CGPROGRAM代码块中变量的声明后添加下面这个函数的代码:
    float2 barrelDistortion(float2 coord) 
    {
      // 镜头畸变算法          
      // 详情 http://www.ssontech.com/content/lensalg.htm
      float2 h = coord.xy - float2(0.5, 0.5);
      float r2 = h.x * h.x + h.y * h.y;
      float f = 1.0 + r2 * (_distortion  * sqrt(r2));
      return f * _scale * h + 0.5;
     }
    
    • 7.我们现在可以专心于 NightVisionEffect 着色器了。给着色器添加一些必要的代码,用于获取渲染纹理和渐晕纹理。在我们着色器中的 frag() 函数中添加下面的代码:
    fixed4 frag(v2f_img i) : COLOR
    {
        //获得渲染纹理的颜色并且获取 v2f_img的uv
        half2 distortedUV = barrelDistortion(i.uv);
        fixed4 renderTex = tex2D(_MainTex, distortedUV);
        fixed4 vignetteTex = tex2D(_VignetteTex, i.uv);
    
    • 8.我们 frag() 函数下一步要处理是扫描线和噪音纹理,并且将一些有合理动画的UV应用到它们身上:
    //处理扫描线和噪音纹理
    half2 scanLinesUV = half2(i.uv.x * _ScanLineTileAmount, i.uv.y * _ScanLineTileAmount);
    fixed4 scanLineTex = tex2D(_ScanLineTex, scanLinesUV);
    half2 noiseUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _NoiseXSpeed), i.uv.y + (_Time.x * _NoiseYSpeed));
    fixed4 noiseTex = tex2D(_NoiseTex, noiseUV);
    
    • 9.为了完成我们的屏幕效果中的所有层级,我们只需要简单的处理渲染纹理的亮度值,然后将夜视颜色应用到它身上从而获得典型的夜视效果外观:
    //利用YIQ值从渲染纹理中获得亮度值
    fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb);
    lum += _Brightness;
    fixed4 finalColor = (lum * 2) + _NightVisionColor;
    
    • 10.最后,我们将所有的层级合到一块,然后返回我们夜视效果的最终颜色:
    //最后的颜色输出
     finalColor = pow(finalColor, _Contrast);
     finalColor *= vignetteTex;
     finalColor *= scanLineTex * noiseTex;
     return finalColor;
    

    当所有的代码都完成后,返回Unity编辑器让脚本和着色器编译。如果没有遇到错误,在编辑器中点击运行然后观察结果。你将会看到跟下图类似的结果:
    diagram


  • 原理介绍
    夜视效果实际上跟老电影屏幕效果很像,向我们展示了我们该如何让这些组件进行模块化。就是简单将用于覆盖的纹理进行交换并且改变计算出来的平铺比例的速度,我们就能用相同的代码获得非常不同的结果。
    唯一不同的地方就是这个效果中我们引入了镜头畸变。这是 SynthEyes 创建者提供给我们的代码片段,你可以在你的效果中免费的使用它:
    float2 barrelDistortion(float2 coord) 
    {
      // 镜头畸变算            
      // 详情 http://www.ssontech.com/content/lensalg.htm
      float2 h = coord.xy - float2(0.5, 0.5);
      float r2 = h.x * h.x + h.y * h.y;
      float f = 1.0 + r2 * (_distortion  * sqrt(r2));
      return f * _scale * h + 0.5;
    }
    

  • 额外内容
    在游戏中高亮具体的游戏对象是很常见的。比如,在人和其他热源之间,隔热板应用的一个后处理效果[可以想象一个红外相机,类似于这样的一个效果:译者注]。应用本书到目前为止所涉及的知识已经可以实现这种效果了。事实上你可以通过代码的方式修改一个对象的材质或者着色器。然而这个过程通常费时费力并且不得不在所有的对象上都执行同样的操作。


    一个更高效的方式是使用可替换着色器。每一个着色器都有一个叫 RenderType 的标签,我们到目前为止都还没有使用过它。这个属性可以用于强制摄像机将一个着色器仅应用在某个具体的游戏对象上。你也可以这样做,只要将下面的代码脚本挂在摄像机上就行:

    using UnityEngine;
    public class ReplacedShader : MonoBehaviour 
    {
      public Shader shader;
      void Start ()
      {
          GetComponent<Camera>().SetReplacementShader(shader, "Heat");
      }
    }
    

    Unity进入运行模式之后,摄像机将会查询所有必须被渲染的游戏对象。如果这些游戏对象没有被 RenderType = “Heat” 修饰的着色器,那么它们将不会被渲染。如果有被这个修饰,并且着色器被挂载在这个脚本上,那么这个游戏对象将会被渲染。