当前位置: 首页 > news >正文

第十一章:游戏玩法和屏幕特效-Gameplay and ScreenEffects《Unity Shaders and Effets Cookbook》

 ​Unity Shaders and Effets Cookbook

《着色器和屏幕特效制作攻略》

这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。

                                                                                                       —— Kenny Lammers


第十一章:游戏玩法和屏幕特效

介绍

第1节.  老电影风格屏幕特效

1.1、准备工作

1.2、如何实现...

1.3、实现原理...

第2节. 夜视屏幕特效

1.1、准备工作

1.2、如何实现...

1.3、实现原理...

1.4、另请参阅


I like this book!


第十一章:游戏玩法和屏幕特效

         在本章节中,你会学习到:

      • 老电影风格屏幕特效
      • 夜视屏幕特效

      介绍

              如果你正在读这本书,你很可能是一个在你那个时代玩过一两场比赛的人。实时游戏的一个方面是让玩家沉浸在一个世界中,感觉就像真的在现实世界中玩一样。当今的游戏会大量的去使用屏幕特效来实现这种沉浸感。

      通过屏幕特效,就可以将某个环境从平静的氛围转成恐怖的氛围,在这里仅仅只需改变屏幕的外观。想象一下,走进一个关卡内的房间,在升到下一级的一瞬间,游戏便会进入片刻的电影风格的效果。 许多现代游戏都会打开不同的屏幕特效来改变当下的氛围感。接下来就让我们来学习如何创建由游戏玩法触发的特效。

      在本章中,我们会了解一些更常见的游戏画面特效。学习如何把游戏的外观从普通特效更改为旧电影特效,同时我们也会了解一下有多少第一人称射击的游戏把夜视特效应用到屏幕上。对于这些案例中的每一个,我们都会研究如何将它们连接到游戏事件中,以便根据当前的游戏演示需要打开和关闭它们。

      第1节.  老电影风格屏幕特效

              许多游戏的世界观都是不同的。有些发生在奇幻世界,或未来的科幻世界,有些甚至发生在旧西部,那时的胶片摄影机才刚刚开发出来,人们观看的电影是黑白的,有时带有所谓的棕褐色特效。外观非常独特,我们将会使用 Unity 中的屏幕特效来复刻这个效果。

              要实现这种效果,需要几个步骤,首先让整个屏幕成为黑和白或者灰度,然后再将此特效分解为组件。下 图1.1 是一部老电影的一些参考镜头,接下来就让我们来分解一下,构成老电影外观的元素:

      图1.1

              这只是我们在网上找到的一些参考图。可以尝试使用 Photoshop 来分解一下实现这样效果的步骤,这样可以帮助我们给新的屏幕特效制定一些计划。这个过程不仅可以告诉我们需要的代码元素都有哪些,还可以让我们快速看到用了哪些混合模式,以及我们将如何构建屏幕特效的图层。我们为此案例创建的 Photoshop 文件包含在本书的支持页面中,位于 www.packtpub.com/support,称为 OldFilmEffect_Research_Layout.psd。

      1.1、准备工作

              接下来让我们看看每个图层是如何组合起来的,同时为我们的着色器和屏幕特效脚本收集一些资源。

              棕褐色调(Sepia Tone):这是一个相对简单的特效,因为我们只需要将原始渲染纹理的所有像素颜色调整到一个颜色范围内。这可以通过调整原始图像的亮度,并添加特定的颜色值来轻松实现。我们的第一层将如下 图1.2 所示:

      图1.2

              暗角特效(Vignette effect):当旧电影放映机放映旧电影时,我们总是可以看到旧电影周围带有渐变的暗边界。这是因为用于电影放映机的灯泡中间的亮度高于胶片边缘的亮度。此特效通常称为暗角特效,是屏幕特效中的第二层。我们可以通过在整个屏幕上叠加纹理来实现这一点。下 图1.3 演示了此图层的画面效果:

      图1.3

              灰尘和划痕(Dust and scratches):我们旧电影屏幕特效的最后一层是灰尘和划痕。该图层将使用两张不同的平铺纹理,一种用于划痕,一种用于灰尘。原因是我们希望随着时间的推移用不同的平铺速率为这两种纹理设置动画。这用来模拟胶片在移动的动画效果,并且旧胶片的每一帧上都有小划痕和灰尘。下 图1.4 演示了此图层的画面效果:

      图1.4

              准备好纹理贴图后,我们开的屏幕特效系统的制作。步骤如下:

      • 1. 收集晕影纹理和一些灰尘和划痕纹理,这里我们已经准备好了。
      • 2. 创建一个名为 OldFilmEffect.cs 的新脚本和一个名为 OldFilmEffectShader.shader 的新着色器。
      • 3. 创建好新文件后,复制第十章的脚本和着色器的代码到新的文件中并保存。

              最后,文件运行没有报错的话,我们就开始修改文件里的代码内容来重新创建这个旧电影特效。

      1.2、如何实现...

              旧电影屏幕特效的单个图层看起来非常简单,但当组合起来时,我们会得到一些视觉上非常令人惊叹的效果。让我们来看看如何为脚本和着色器构建代码,然后逐步分析每一行代码,并了解为什么事情会以这种方式工作。此时,你可以启动并运行屏幕特效系统,运行的的方法在第10章已经有详细讲解,这里就不过多赘述。

      • 步骤1. 我们首先在脚本中输入代码。输入的第一个代码块来定义在 Inspector 属性面板中公开的变量,这样我们就可以在 Unity 编辑器中实时的调整效果。同时我们还可以使用模拟的 Photoshop 文件作为参考。在特效脚本中输入以下代码:
      #region Variables
      public Shader curShader;public float oldFilmEffectAmount = 1.0f;public Color sepiaColor = Color.white;
      public Texture2D blendTexture;
      public float vignetteAmount = 1.0f;public Texture2D scratchesTexture;
      public float scratchesYSpeed = 10.0f;
      public float scratchesXSpeed = 10.0f;private Material curMaterial;
      public float randomValue;
      #endregion
      • 步骤2. 接下来,我们需要填写 OnRenderImage() 函数的内容。在此函数中,我们可以通过数据从变量传递给着色器,以便着色器可以在渲染纹理的处理中使用到该数据:
      void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
      {if(oldFilmShader != null){material.SetColor("_SepiaColor", sepiaColor);material.SetFloat("_VignetteAmount",vignetteAmount);material.SetFloat("_EffectAmount",oldFilmEffectAmount);if(vigntteTexture){material.SetTexture("_VignetteTex", vigntteTexture);}if(scratchesTexture){material.SetTexture("_ScratchesTex", scratchesTexture);material.SetFloat("_EffectAmount",oldFilmEffectAmount);material.SetFloat("_EffectAmount",oldFilmEffectAmount);}if(dustTexture){material.SetTexture("_DustTex", dustTexture);material.SetFloat("_DustYSpeed",dustYSpeed);material.SetFloat("_DustXSpeed",dustXSpeed);material.SetFloat("_RandomValue",randomValue);}Graphics.Blit(sourceTexture, destTexture, material);}else{Graphics.Blit(sourceTexture, destTexture);}
      }
      • 步骤3. 要完成此特效的脚本部分,我们只需要限制我们需要去限制的变量值,而不是所有的变量值都需要去做范围上的限制。
      void Update()
      {vignetteAmount = Mathf.Clamp01(vignetteAmount);oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0f, 1.5f);randomValue = Random.Range(-1f, 1f);
      }
      • 完整代码:
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;[ExecuteInEditMode]
      public class TestRenderImage_OldMovie : MonoBehaviour
      {#region Variablespublic Shader oldFilmShader;public float oldFilmEffectAmount = 1.0f;public Color sepiaColor = Color.white;public Texture2D vigntteTexture;public float vignetteAmount = 1.0f;public Texture2D scratchesTexture;public float scratchesYSpeed = 10.0f;public float scratchesXSpeed = 10.0f;public Texture2D dustTexture;public float dustYSpeed = 10.0f;public float dustXSpeed = 10.0f;private Material curMaterial;public float randomValue;#endregion#region PropertiesMaterial material{get{if(curMaterial == null){curMaterial = new Material(oldFilmShader);curMaterial.hideFlags = HideFlags.HideAndDontSave;}return curMaterial;}}#endregion// Start is called before the first frame updatevoid Start(){if(!SystemInfo.supportsImageEffects){enabled = false;return;}if(!oldFilmShader && !oldFilmShader.isSupported){enabled = false;}}void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture){if(oldFilmShader != null){material.SetColor("_SepiaColor", sepiaColor);material.SetFloat("_VignetteAmount",vignetteAmount);material.SetFloat("_EffectAmount",oldFilmEffectAmount);if(vigntteTexture){material.SetTexture("_VignetteTex", vigntteTexture);}if(scratchesTexture){material.SetTexture("_ScratchesTex", scratchesTexture);material.SetFloat("_ScratchesYSpeed",oldFilmEffectAmount);material.SetFloat("_ScratchesXSpeed",oldFilmEffectAmount);}if(dustTexture){material.SetTexture("_DustTex", dustTexture);material.SetFloat("_DustYSpeed",dustYSpeed);material.SetFloat("_DustXSpeed",dustXSpeed);material.SetFloat("_RandomValue",randomValue);}Graphics.Blit(sourceTexture, destTexture, material);}else{Graphics.Blit(sourceTexture, destTexture);}}// Update is called once per framevoid Update(){vignetteAmount = Mathf.Clamp01(vignetteAmount);oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0f, 1.5f);randomValue = Random.Range(-1f, 1f);}void OnDisable(){if(curMaterial){if(curMaterial){DestroyImmediate(curMaterial);}}}}
      
      • 步骤4. 脚本完成后,就开始编写着色器的代码。我们需要在着色器中创建相应的变量,这些变量是在脚本中创建的那些变量。这可以允许脚本和着色器之间相互通信。在着色器的 Properties 块中输入以下代码:
      Properties
      {_MainTex ("Base(RGB)", 2D) = "white" {}_VignetteTex ("Vignette Texture", 2D) = "white" {}_ScratchesTex ("Scratches Texture", 2D) = "white" {}_DustTex ("Dust Texture", 2D) = "white" {}_SepiaColor("Sepia Color", Color) = (1,1,1,1)_EffectAmount("Old Film Effect Amount", Range(0,1)) = 1.0_VignetteAmount("Vignette Opacity", Range(0,1)) = 1.0_ScratchesYSpeed("Scratches Y Speed", Float) = 10.0_ScratchesXSpeed("Scratches X Speed", Float) = 10.0_DustYSpeed("Dust Y Speed", Float) = 10.0_DustXSpeed("Dust Y Speed", Float) = 10.0_RandomValue("Random Value", Float) = 10.0
      }
      • 步骤5. 然后像往常一样,我们需要将这些相同的变量名称添加到我们的 CGPROGRAM 块中,以便 Properties 块可以与 CGPROGRAM 块通信:
      SubShader
      {Pass{CGPROGRAM#pragma vertex vert_img#pragma fragment frag#pragma fragmentoption APB_precision_hint_fastest#include "UnityCG.cginc"sampler2D _MainTex;sampler2D _VignetteTex;sampler2D _ScratchesTex;sampler2D _DustTex;half4 _SepiaColor;float _EffectAmount;float _VignetteAmount;float _ScratchesYSpeed;float _ScratchesXSpeed;float _DustYSpeed;float _DustXSpeed;float _RandomValue;
      • 步骤6. 现在,我们开始编写 frag() 函数,来处理屏幕特效的像素。首先,让我们采样脚本传递给我们的渲染纹理和晕影纹理:
      half4 frag (v2f_img i) : COLOR
      {// 从renderTexture中获取颜色,从v2f_img结构中获取uv half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005));half4 renderTex = tex2D(_MainTex, renderTexUV);// 采样暗角纹理贴图half4 vignetteTex = tex2D(_VignetteTex, i.uv);
      • 步骤7. 然后,我们需要通过输入以下代码添加灰尘和划痕的效果:
      // 计算划痕的uv 和划痕的纹理采样
      half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed),i.uv.y + (_Time.x * _ScratchesYSpeed));
      half4 scratchesTex = tex2D(_ScratchesTex, scratchesUV);// 计算尘埃的uv 和尘埃的纹理采样
      half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _DustXSpeed)),i.uv.y + (_Time.x * _DustYSpeed));
      half4 dustTex = tex2D(_DustTex, dustUV);
      • 步骤8. 下边是 棕褐色调 :
      // 使用YIO值从渲染纹理中获取亮度值
      half lum = dot(half3(0.299, 0.587, 0.114), renderTex.rgb);// 把lum值和一个常量颜色值相加
      half4 finalColor = lum + lerp(_SepiaColor, _SepiaColor +half4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue);
      • 步骤9. 最后,我们将所有的图层和颜色组合在一起,并返回最终的屏幕特效渲染值:
      // 创建一个常量,我们使用它来调整效果的不透明度
      half3 constantWhite = half3(1,1,1);// 把不同的图层合并到一起,来创建最终的屏幕特效
      finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount);
      finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue));
      finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue));
      finalColor = lerp(renderTex, finalColor, _EffectAmount);return finalColor;
      }
      ENDCG
      • 完整代码:
      Shader "CookbookShaders/11-1 Old Movie"
      {Properties{_MainTex ("Base(RGB)", 2D) = "white" {}_VignetteTex ("Vignette Texture", 2D) = "white" {}_ScratchesTex ("Scratches Texture", 2D) = "white" {}_DustTex ("Dust Texture", 2D) = "white" {}_SepiaColor("Sepia Color", Color) = (1,1,1,1)_EffectAmount("Old Film Effect Amount", Range(0,1)) = 1.0_VignetteAmount("Vignette Opacity", Range(0,1)) = 1.0_ScratchesYSpeed("Scratches Y Speed", Float) = 10.0_ScratchesXSpeed("Scratches X Speed", Float) = 10.0_DustYSpeed("Dust Y Speed", Float) = 10.0_DustXSpeed("Dust Y Speed", Float) = 10.0_RandomValue("Random Value", Float) = 10.0}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{CGPROGRAM#pragma vertex vert_img#pragma fragment frag#pragma fragmentoption APB_precision_hint_fastest#include "UnityCG.cginc"sampler2D _MainTex;sampler2D _VignetteTex;sampler2D _ScratchesTex;sampler2D _DustTex;half4 _SepiaColor;float _EffectAmount;float _VignetteAmount;float _ScratchesYSpeed;float _ScratchesXSpeed;float _DustYSpeed;float _DustXSpeed;float _RandomValue;struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};half4 frag (v2f_img i) : COLOR{// 从renderTexture中获取颜色,从v2f_img结构中获取uv half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005));half4 renderTex = tex2D(_MainTex, renderTexUV);// 采样暗角纹理贴图half4 vignetteTex = tex2D(_VignetteTex, i.uv);// 计算划痕的uv 和划痕的纹理采样half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed),i.uv.y + (_Time.x * _ScratchesYSpeed));half4 scratchesTex = tex2D(_ScratchesTex, scratchesUV);// 计算尘埃的uv 和尘埃的纹理采样half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _DustXSpeed)),i.uv.y + (_Time.x * _DustYSpeed));half4 dustTex = tex2D(_DustTex, dustUV);// 使用YIO值从渲染纹理中获取亮度值half lum = dot(half3(0.299, 0.587, 0.114), renderTex.rgb);// 把lum值和一个常量颜色值相加half4 finalColor = lum + lerp(_SepiaColor, _SepiaColor +half4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue);// 创建一个常量,我们使用它来调整效果的不透明度half3 constantWhite = half3(1,1,1);// 把不同的图层合并到一起,来创建最终的屏幕特效finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount);finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue));finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue));finalColor = lerp(renderTex, finalColor, _EffectAmount);return finalColor;}ENDCG}}
      }
      • 步骤10. 如果代码没有报错,你应该会得到与下 图1.5 非常相似的渲染结果。我们可以在在 Unity 编辑器中点击 Play,查看渲染结果。

      图1.5

      1.3、实现原理...

              现在,让我们浏览一下屏幕特效中的每一层,并分析每行代码为什么都以这种方式工作,还深入了解一下如何向此屏幕特效内添加更多内容。

              现在我们的旧电影屏幕特效已经显示出渲染结果了,下面让我们逐步浏览一下 frag() 函数中的代码行。

              就像我们的 Photoshop 图层一样,我们的着色器会去处理每个图层,然后将它们合成在一起,因此当我们浏览每个图层时,可以尝试想一下 Photoshop 中的图层是如何工作的。牢记这个概念总归会有助于去制作新的屏幕特效。

              我们来看一下,在 frag() 函数中的第一组代码:

      half4 frag (v2f_img i) : COLOR

      {

          // 从renderTexture中获取颜色,从v2f_img结构中获取uv

          half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005));

          half4 renderTex = tex2D(_MainTex, renderTexUV);

          // 采样暗角纹理贴图

          half4 vignetteTex = tex2D(_VignetteTex, i.uv);

              第一行代码,定义了主渲染纹理的 UV 值。我们想模拟旧电影风格的效果,因此我们需要调整渲染纹理的UV,让其每一帧都闪烁。也就是对 UV 做了动画的效果。

              我们使用 Unity 提供的内置 _SinTime 变量来获取 -1 和 1 之间的值。然后,我们再将其乘以一个非常小的数值,即乘以0.005 ,用来降低速度的强度。然后,用最终值再次乘以我们在 Effect 脚本中生成的 _RandomValue 变量。该值在 -1 和 1 之间来回反弹,也就是说可以来回翻转运动的方向。

              UV计算完成之后,我们就可以使用 tex2D() 函数对渲染纹理进行采样。然后,以此作为最终的渲染纹理,我们可以使用它在着色器的其余部分中进一步处理。

              对于暗角纹理我们只需使用 tex2D() 函数对暗角纹理进行直接采样即可。我们不需要使用刚刚新创建的动画UV值,因为暗角纹理会与摄像机本身的运动相关联,不会与摄像机胶片的动画相关联。

              以下代码片段展示了 frag() 函数中的第二组代码:

      // 计算划痕的uv 和划痕的纹理采样

      half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed),

                                  i.uv.y + (_Time.x * _ScratchesYSpeed));

      half4 scratchesTex = tex2D(_ScratchesTex, scratchesUV);

      // 计算尘埃的uv 和尘埃的纹理采样

      half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _DustXSpeed)),

                          i.uv.y + (_Time.x * _DustYSpeed));

      half4 dustTex = tex2D(_DustTex, dustUV);

              这些代码行几乎与前面的代码行一模一样,其中我们需要计算带动画的 UV 值,让屏幕特效图层的位置动起来。我们只需使用内置的 _SinTime 值来获得一个介于 -1 和 1 之间的值,再将其乘以我们的随机值,然后再乘以另一个变量值来调整动画的整体速度。完成 UV 之后,我们就可以使用这些新的动画值对灰尘和划痕纹理进行采样。

              我们的下一组代码是为处理旧的电影屏幕特效所创建着色效果。如下代码所示:

      // 使用YIO值从渲染纹理中获取亮度值

      half lum = dot(half3(0.299, 0.587, 0.114), renderTex.rgb);

      // 把lum值和一个常量颜色值相加

      half4 finalColor = lum + lerp(_SepiaColor, _SepiaColor +

                                      half4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue);

              使用这组代码,我们正在创建整个渲染纹理的实际颜色着色。为此,我们先把渲染纹理转换为灰度模式。然后,我们可以通过 YIQ 的值来给我们提供的亮度值。YIQ 值是 NTSC 彩色电视系统使用的色彩空间。YIQ 中的每个字母实际上都存储了电视用来调整颜色以提高可读性的颜色常量。有关 YIQ 值的更多信息,请参阅以下链接:

      http://en.wikipedia.org/http://en.wikipedia.org/

      http://www.blackice.com/http://www.blackice.com/

      http://dcssrv1.oit.uci.edu/http://dcssrv1.oit.uci.edu/

      虽然没有必要知道这种色标的真正的原因是什么,但应该知道 YIQ 中的 Y 值是任何图像的恒定亮度值。因此,我们可以通过获取渲染纹理中的每个像素,并用我们的亮度值点缀它来生成渲染纹理的灰度图像。这就是这组代码第一行正在做的事情。

              一旦我们有了亮度值,我们就可以简单地添加我们想要为图像着色的颜色。这种颜色从脚本传递到我们的着色器中,然后再传递到我们的 CGPROGRAM 块中,我们可以将其添加到我们的灰度渲染纹理中。完成后,我们就会拥有一个完美着色的图像。

              最后,我们在屏幕特效中创建每个图层之间的混合。代码如下所示:

      // 创建一个常量,我们使用它来调整效果的不透明度

          half3 constantWhite = half3(1,1,1);

          // 把不同的图层合并到一起,来创建最终的屏幕特效

          finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount);

          finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue));

          finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue));

          finalColor = lerp(renderTex, finalColor, _EffectAmount);

          return finalColor;

      }

      ENDCG

              我们的最后一组代码相对简单,不需要大量的解释。简而言之,它只是将所有层相乘,以得出我们的最终渲染结果。就像我们在 Photoshop 中将图层相乘的混合模式一样,只不过目前我们是在着色器中将它们相乘。每个图层都会通过 lerp() 函数进行处理,以便我们可以调整每个图层的不透明度,从而对最终特效进行更多的艺术控制。可以提供的调整越多,屏幕特效的渲染效果就会越好。

      第2节. 夜视屏幕特效

              我们的下一个屏幕特效绝对是更受欢迎的。夜视屏幕特效在《使命召唤:现代战争》、《光环》以及当今市场上的几乎所有第一人称射击游戏中都是可以看到的。这是使用非常独特的柠檬绿色,它是可以让整个图像变亮的特效。

              为了实现夜视特效,我们可以使用 Photoshop 来分解一下此特效的效果。这是一个比较简单的过程,可以在网络中直接查找一些参考图像,并使用分层的形式来合成图像,目的是来查看你需要什么样的混合模式,或者我们需要以什么顺序组合图层。下图显示了在 Photoshop 中此特效的效果图,如 图2.1 所示:

      图2.1

              让我们开始分解一下 Photoshop 中的效果图的组成部分,以便我们来收集一些所需要的资产。在下一小节中,我们将开始讲解制作的过程。

      1.1、准备工作

              我们还是在 Photoshop 软件中对此屏幕特效进行分析和分解其组件,这样可以更好的来理解每一个步骤。

              尖锐的绿色(Tinted green):我们的第一层是一个标志性的绿色画面,几乎每张夜视图像中都是使用的这个颜色。这也成为了此特效的一个标志,如下 图2.2 所示:

      图2.2

              扫描线(Scan lines):为了给玩家增加新的感官效果,我们在着色层的最上边添加了扫描线画面,如下 图2.3 所示:

      图2.3

              噪点(Noise):下一层是一张简单的噪点纹理,我们将其平铺在所有颜色图像和扫描线上,用它来增加画面的细节。

      图2.4

              暗角(Vignette):夜视特效的最后一层是暗角。如果你看看《使命召唤现代战争》中的夜视特效,你会注意到它使用了一个假装俯视瞄准镜头特效的小插曲。我们也为此屏幕特效做同样的事情。

      图2.5

              让我们使用以上的纹理来开始创建屏幕特效系统,步骤如下所示:

      • 1. 制作或收集暗角纹理、噪点纹理和扫描线纹理,这里我们已经收集好了三张纹理图。
      • 2. 创建一个名为 NightVisionEffect.cs 的新脚本和一个名为 NightVisionEffectShader.shader 的新着色器。
      • 3. 创建完成后,把之前的代码复制到相对应的文件中。

              最后,文件运行没有报错的话,我们就开始修改文件里的代码内容来重新创建这个旧电影特效。

      1.2、如何实现...

              收集完所有资源后,让我们开始为脚本和着色器添加新的代码,打开 C# 脚本。

      • 步骤1. 我们需要创建一些变量,允许我们在检查器中对脚本的属性进行调整。在NightVisionEffect.cs脚本中输入以下代码:
      #region Variables
      public Shader nightVisionShader;public float contrast = 2.0f;
      public float brightness = 1.0f;
      public Color nightVisionColor = Color.white;public Texture2D vigntteTexture;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;public float randomValue = 0.0f;
      private Material curMaterial;
      #endregion
      • 步骤2. 接下来,我们编写 OnRenderImage() 函数里的代码,以便将正确的数据传递给着色器,这样着色器就可以正确的处理屏幕特效。在 OnRenderImage() 函数块中输入以下代码:
      void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
      {if(nightVisionShader != null){material.SetFloat("_Contrast",contrast);material.SetFloat("_Brightness",brightness);material.SetColor("_NightVisionColor", nightVisionColor);material.SetFloat("_RandomValue",randomValue);material.SetFloat("_Distortion",distortion);material.SetFloat("_Scale",scale);if(vigntteTexture){material.SetTexture("_VignetteTex", vigntteTexture);}if(scanLineTexture){material.SetTexture("_ScanLineTex", scanLineTexture);material.SetFloat("_ScanLineTileAmount", scanLineTileAmount);}if(nightVisionNoise) {material.SetTexture("_NoiseTex", nightVisionNoise);material.SetFloat("_NoiseXSpeed", noiseXSpeed);material.SetFloat("_NoiseYSpeed", noiseYSpeed);}Graphics.Blit(sourceTexture, destTexture, material);}else{Graphics.Blit(sourceTexture, destTexture);}
      }
      
      • 步骤3. 要完成NightVisionEffect.cs脚本,我们只需要确保把某些变量限制在一定范围内。这些范围是任意的,也可以在以后更改。
      void Update()
      {contrast = Mathf.Clamp(contrast, 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);
      }
      • 完整代码:
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;[ExecuteInEditMode]
      public class TestRenderImage_NightVisionScreen : MonoBehaviour
      {#region Variablespublic Shader nightVisionShader;public float contrast = 2.0f;public float brightness = 1.0f;public Color nightVisionColor = Color.white;public Texture2D vigntteTexture;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;public float randomValue = 0.0f;private Material curMaterial;#endregion#region PropertiesMaterial material{get{if(curMaterial == null){curMaterial = new Material(nightVisionShader);curMaterial.hideFlags = HideFlags.HideAndDontSave;}return curMaterial;}}#endregion// Start is called before the first frame updatevoid Start(){if(!SystemInfo.supportsImageEffects){enabled = false;return;}if(!nightVisionShader && !nightVisionShader.isSupported){enabled = false;}}void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture){if(nightVisionShader != null){material.SetFloat("_Contrast",contrast);material.SetFloat("_Brightness",brightness);material.SetColor("_NightVisionColor", nightVisionColor);material.SetFloat("_RandomValue",randomValue);material.SetFloat("_Distortion",distortion);material.SetFloat("_Scale",scale);if(vigntteTexture){material.SetTexture("_VignetteTex", vigntteTexture);}if(scanLineTexture){material.SetTexture("_ScanLineTex", scanLineTexture);material.SetFloat("_ScanLineTileAmount", scanLineTileAmount);}if(nightVisionNoise) {material.SetTexture("_NoiseTex", nightVisionNoise);material.SetFloat("_NoiseXSpeed", noiseXSpeed);material.SetFloat("_NoiseYSpeed", noiseYSpeed);}Graphics.Blit(sourceTexture, destTexture, material);}else{Graphics.Blit(sourceTexture, destTexture);}}// Update is called once per framevoid Update(){contrast = Mathf.Clamp(contrast, 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);}void OnDisable(){if(curMaterial){if(curMaterial){DestroyImmediate(curMaterial);}}}}
      
      • 步骤4. 接下来让我们编写着色器部分。打开着色器,然后在 属性(Properties) 块中输入以下属性:
      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 块传递到 CGPROGRAM 块中,我们需要在 CGPROGRAM 块中使用相同的名称声明它们。
      SubShader
      {Pass{CGPROGRAM#pragma vertex vert_img#pragma fragment frag#pragma fragmentoption APB_precision_hint_fastest#include "UnityCG.cginc"sampler2D _MainTex;sampler2D _VignetteTex;sampler2D _ScanLineTex;sampler2D _NoiseTex;half4 _NightVisionColor;float _Contrast;float _ScanLineTileAmount;float _Brightness;float _RandomValue;float _NoiseXSpeed;float _NoiseYSpeed;float _Distortion;float _Scale;
      • 步骤6. 我们的特效还包括了镜头失真效果,以进一步传达我们通过镜头观看并且图像边缘因镜头角度而失真的特效。在 CGPROGRAM 块中的变量减速之后输入以下函数:
      float2 barrelDistortion(float2 coord)
      {// 镜头扭曲算法// 网站链接:http://www.ssontech.com/content/lensalg.htmfloat2 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() 函数中输入以下代码:
      half4 frag (v2f_img i) : COLOR
      {// 从renderTexture中获取颜色,从v2f_img结构中获取uv half2 distortedUV = barrelDistortion(i.uv);half4 renderTex = tex2D(_MainTex, distortedUV);half4 vignetteTex = tex2D(_VignetteTex, i.uv);
      • 步骤8. 接下来我们采样扫描线纹理和噪点纹理,对两个采样的纹理分别计算它们的 UV 动画值:
      // 计算扫描线纹理和噪点纹理half2 scanLinesUV = half2(i.uv.x * _ScanLineTileAmount, i.uv.y * _ScanLineTileAmount);half4 scanLineTex = tex2D(_ScanLineTex, scanLinesUV);half2 noiseUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _NoiseXSpeed)),i.uv.y + (_Time.x * _NoiseYSpeed));half4 noiseTex = tex2D(_NoiseTex, noiseUV);
      • 步骤9. 下面我们只需要计算渲染纹理的亮度值,然后和夜视颜色相加即可:
      // 使用YIO值从渲染纹理中获取亮度值
      half lum = dot(half3(0.299, 0.587, 0.114), renderTex.rgb);
      lum += _Brightness;
      half4 finalColor = (lum * 2) + _NightVisionColor;
      • 步骤10. 最后,我们将所有图层组合在一起并返回最终的颜色值:
          // 输出最终颜色值finalColor = pow(finalColor, _Contrast);finalColor *= vignetteTex;finalColor *= scanLineTex * noiseTex;return finalColor;
      }
      ENDCG
      • 完整代码:
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;[ExecuteInEditMode]
      public class TestRenderImage_NightVisionScreen : MonoBehaviour
      {#region Variablespublic Shader nightVisionShader;public float contrast = 2.0f;public float brightness = 1.0f;public Color nightVisionColor = Color.white;public Texture2D vigntteTexture;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;public float randomValue = 0.0f;private Material curMaterial;#endregion#region PropertiesMaterial material{get{if(curMaterial == null){curMaterial = new Material(nightVisionShader);curMaterial.hideFlags = HideFlags.HideAndDontSave;}return curMaterial;}}#endregion// Start is called before the first frame updatevoid Start(){if(!SystemInfo.supportsImageEffects){enabled = false;return;}if(!nightVisionShader && !nightVisionShader.isSupported){enabled = false;}}void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture){if(nightVisionShader != null){material.SetFloat("_Contrast",contrast);material.SetFloat("_Brightness",brightness);material.SetColor("_NightVisionColor", nightVisionColor);material.SetFloat("_RandomValue",randomValue);material.SetFloat("_Distortion",distortion);material.SetFloat("_Scale",scale);if(vigntteTexture){material.SetTexture("_VignetteTex", vigntteTexture);}if(scanLineTexture){material.SetTexture("_ScanLineTex", scanLineTexture);material.SetFloat("_ScanLineTileAmount", scanLineTileAmount);}if(nightVisionNoise) {material.SetTexture("_NoiseTex", nightVisionNoise);material.SetFloat("_NoiseXSpeed", noiseXSpeed);material.SetFloat("_NoiseYSpeed", noiseYSpeed);}Graphics.Blit(sourceTexture, destTexture, material);}else{Graphics.Blit(sourceTexture, destTexture);}}// Update is called once per framevoid Update(){contrast = Mathf.Clamp(contrast, 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);}void OnDisable(){if(curMaterial){if(curMaterial){DestroyImmediate(curMaterial);}}}}
      

              代码完成后,返回 Unity 编辑器,让脚本和着色器进行编译。如果没有报错,请点击编辑器中的“播放”按钮来查看渲染结果,如下 图2.3 所示:

      图2.6

      1.3、实现原理...

       夜视效果实际上非常类似于老电影的屏幕效果,这向我们展示了如何去模块化这些组件。 仅仅通过简单地交换我们叠加的纹理和改变我们的平铺率计算的速度,就可以使用相同的代码实现非常不同的结果。

              这种效果的唯一区别是,我们在屏幕特效中加入了镜头扭曲效果。 让我们把它分解一下,这样我们就能更好地理解它是如何工作的。

              下面的代码片段展示了镜头扭曲的代码。 这是SynthEyes的制作者提供给我们的代码片段,并且代码可以免费用到你的游戏制作中:

              我们来分析一下 barrelDistortion() 函数,第一行代码表示找到渲染纹理图像的中心。 一旦我们有了图像的中心,原理图像中心的像素我们就可以对其进行拉伸。 因此,我们模拟了主渲染纹理被镜头角度扭曲的效果。 应用到夜视特效等屏幕效果时是相当不错的。

      1.4、另请参阅

      以下链接可以带你了解 镜头扭曲的特效 的实现方式:

      dcssrv1.oit.uci.eduhttp://www.ssontech.com/content/lensalg.htmwww.blackice.comhttp://www.ssontech.com/content/lensalg.htm


      ​这本书可以让你学习到如何使用着色器和屏幕特效让你的Unity工程拥有震撼的渲染画面。

      作者:Kenny Lammers


      文章转载自:

      http://dxJfohwH.qykss.cn
      http://sk8VyWl2.qykss.cn
      http://EBhj8xO9.qykss.cn
      http://5fcjfcdG.qykss.cn
      http://6wTvDsh5.qykss.cn
      http://taZH4OLl.qykss.cn
      http://7hAFAuEN.qykss.cn
      http://SjqiCWvs.qykss.cn
      http://8YYmo4w9.qykss.cn
      http://dzY1Ibrl.qykss.cn
      http://Cpi6sV2Y.qykss.cn
      http://bvIecnZh.qykss.cn
      http://TEUVFUsI.qykss.cn
      http://kf1vUXoY.qykss.cn
      http://OShHDV2G.qykss.cn
      http://Xew4nrk5.qykss.cn
      http://CxN7TRTO.qykss.cn
      http://sah9sDoh.qykss.cn
      http://UGYvvVfG.qykss.cn
      http://GyWoQos9.qykss.cn
      http://tcAtyEgX.qykss.cn
      http://feAabW9p.qykss.cn
      http://jbIgOWZC.qykss.cn
      http://IwMFLiYW.qykss.cn
      http://4zkVnBNJ.qykss.cn
      http://LQ6TlaxB.qykss.cn
      http://sk4A72sG.qykss.cn
      http://egOKe6ge.qykss.cn
      http://XQuDQz1T.qykss.cn
      http://DtZduZWv.qykss.cn
      http://www.dtcms.com/a/386642.html

      相关文章:

    • Choerodon UI V1.6.7发布!为 H-ZERO 开发注入新动能
    • 科教共融,具创未来!节卡助力第十届浦东新区机器人创新应用及技能竞赛圆满举行
    • 食品包装 AI 视觉检测技术:原理、优势与数据应用解析
    • 【深度学习计算机视觉】05:多尺度目标检测之FPN架构详解与PyTorch实战
    • 从工业革命到人工智能:深度学习的演进与核心概念解析
    • [Emacs list使用及配置]
    • DQN在稀疏奖励中的局限性
    • 为何需要RAII——从“手动挡”到“自动挡”的进化
    • 第五课、Cocos Creator 中使用 TypeScript 基础介绍
    • 09MYSQL视图:安全高效的虚拟表
    • R 语言本身并不直接支持 Python 中 f“{series_matrix}.txt“ 这样的字符串字面量格式化(f-string)语法 glue函数
    • 【AI论文】AgentGym-RL:通过多轮强化学习训练大语言模型(LLM)智能体以实现长期决策制定
    • Win11本地jdk1.8和jdk17双版本切换运行方法
    • vue3 使用print.js打印el-table全部数据
    • Vue 3 + TypeScript + 高德地图 | 实战:多车轨迹回放(点位驱动版)
    • [vue]创建表格并实现筛选和增删改查功能
    • JVM-运行时内存
    • 后缀树跟字典树的区别
    • LanceDB向量数据库
    • RabbitMQ 异步化抗洪实战
    • 《Java集合框架核心解析》
    • 二维码生成器
    • OSI七层模型
    • 【原创·极简新视角剖析】【组局域网】设备在同一局域网的2个条件
    • 第8课:高级检索技术:HyDE与RAG-Fusion原理与DeepSeek实战
    • Windows 命令行:路径的概念,绝对路径
    • 异常检测在网络安全中的应用
    • 【ubuntu】ubuntu 22.04 虚拟机中扩容操作
    • 【数值分析】05-绪论-章节课后1-7习题及答案
    • Java NIO 核心机制与应用