第九章 游戏和屏幕效果 当我们要创建可信和沉浸的游戏的时候,我们要考虑的不仅仅只有材料。屏幕效果也会改变游戏的整体感觉。这在电影里面非常常见,比如后期制作阶段中的调色。使用 第八章 通过Unity渲染纹理实现屏幕效果 中学到的知识,你也可以在游戏中实现这些技术。在这一章将会呈现两个有趣的效果;当然,你可以适当修改它们以适用于你的需求,也可以创建完全属于你自己屏幕效果。
在这一章,你将会学到下面的这些知识点:
创建一个老电影屏幕效果 创建一个夜视屏幕效果 介绍
如果你正在阅读这本书,你很可能玩过一两个游戏。即时游戏一方面会使玩家进入一个沉浸世界,让人觉得他们好像在现实世界玩游戏一样。现代的游戏利用的屏幕效果越多获得的沉浸感也越多。 通过屏幕效果,我们可以将在某个确切环境中的心境从平静转为惊恐,仅仅只要改变屏幕看起来的样子。想象一下走进了某个关卡中的房间,然后游戏突然接管并且将你带进一个电影时刻。很多现代游戏都会使用不同的屏幕效果来改变不同时刻的一个心境。理解如何创建在游戏中使用的效果是我们学习编写着色器的下一个旅程。 在这一章,我们将了解一些更加常用的游戏中的屏幕效果。你将会学习如何改变游戏的样子,把它从正常的样子改成一个老电影效果的样子,并且我们还会去了解大多数 FPS(first-person shooter第一人称射击) 游戏是如何使他们的夜视效果呈现在屏幕中的。通过这些知识点,我们将了解如何将这些效果跟游戏中的事件关联起来,好让游戏根据当前演出的需要去打开或者关闭这些特效。 创建一个老电影屏幕效果 很多游戏背景设定在不同的时期。有些发生在幻想世界或者科幻世界,更有甚者发生在旧西部,那个时候电影摄像机才刚刚发明并且人们看到的都是一些黑白电影或者棕褐色效果色调的电影。它们看起来格外不同,我们将在Unity中用屏幕效果来复制这种看起来的样子。
获得这种效果需要一些步骤,如果要将整个屏幕变成黑或白或灰,我们需要将这个效果分解成不同的组成部分。如果我们分析一些相关的老电影的镜头,我们就可以开始做这个了。让我们来看看下面这张图片并且分解其中的元素,看看是那些构成了这个老电影的样子:
我们用一些在网上找到的图片构建了这个图片。像这样尝试利用Photoshop来构建图片总是一个很好的主意,它能为你的新的屏幕特效打好一个计划。在这个过程中它不仅能将我们需要用代码编写的元素告诉我们,还提供了一个快捷的方式让我们了解我们的屏幕效果需要使用哪一种混合模式和我们将要构建那些层级。本书这个知识点中我们为Photoshop创建的这些文件的支持网站在http://www.packtpub.com/support[已经失效(译者注)]。它是一个名为 OldFilmEffect_Research_Layout.psd 的文件。
始前准备
我们现在目的明确,让我们看看最终效果的每一层都包含了什么然后为我们的着色器和C#脚本收集一些资源。
复古色调(Sepia tone): 这是一个相对容易获得的效果,我们只需把原始渲染纹理的所有像素颜色变为一个单一的颜色范围。使用原始图像的亮度然后加上一个常量颜色就可以很容易获得。我们的第一层将会看起来跟下面的图片一样:
渐晕效果(Vignette effect): 当一些古老的电影放映机放映老电影的时候,我们经常看到某种软边界[ 我个人觉得翻译的不够准确 ]围绕在老电影的四周。这是因为这种电影放映机使用的球形灯泡发出的光中间部位比周围要亮造成的。这种效果通常叫做渐晕效果并且是我们屏幕效果的第二层。我们可以通过在整个屏幕上覆盖一张纹理来获得这个效果。下面的图片演示了这一层看起来的样子,就是一张纹理:
灰尘和划痕(Dust and scratches): 第三层也是最后一层就是我们的老电影屏幕效果中的灰尘和划痕。这一层将使用两种不同的平铺纹理,一种作为划痕然后另一种作为灰尘。原因就是我们将根据时间以不同的速度对这两种纹理做动画。这将会产生一种效果,就是当电影在播放的时候同时在老电影的每一帧中都会有一些细小的划痕和灰尘。下图演示了它们的纹理看起来效果:
我们用前面的那些纹理来准备好我们的 屏幕效果系统(screen effect system)。按照下面的步骤来:
1.将 渐晕纹理(vignette texture) 和 灰尘划痕纹理(dust and scratches texture) 收集起来,就像我们前面看到的那几张。 2.创建一个名为 OldFilmEffect.cs 的新脚本和一个名为 OldFilmEffectShader.shader 的新着色器。 3.创建好这些新的文件后,给它们编写需要的代码来完成我们的屏幕效果系统并且顺利的跑起来。想知道具体如何做,可以参考 第八章 通过Unity渲染纹理实现屏幕效果 来了解。 最后,随着我们的屏幕效果系统完成并且顺利跑起来,以及收集好了我们的纹理,我们就可以开始复现老电影效果的制作过程了。
操作步骤
老电影屏幕效果的那些独特层级都很简单,但是将它们组合之后我们就能获得令人震惊的视觉效果。让我们缕一缕该怎么构建我们的脚本和着色器,之后我们就能逐行解析并且学习为什么可以那样写。此时,你应该有个设置好的屏幕效果系统而且能顺利运行,因为我们这个知识点不会涵盖如何设置这个系统。 1.我们将添加脚本代码。我们要输入的第一个代码块将定义我们的变量,这些变量会在 检查器 (Inspector) 上显示,好让这个效果的使用者可以可以用想填的数据修改它们。如我们还想在检查器上显示我们效果用到的那些我们处理好的Photoshop文件,也可以在此添加它们的引用。在脚本中添加下面的代码: #region Variables public Shader oldFilmShader; public float OldFilmEffectAmount = 1.0f; public float contrast = 3.0f; public float distortion = 0.2f; public float cubicDistortion = 0.6f; public float scale = 0.8f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; 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; private float randomValue; #endregion 2.接下来,我们需要修改 OnRenderImage() 方法中的内容了。在这里,我们将把脚本中变量的数据传给着着色器好让着色器在处理渲染纹理的时候可以使用这些数据: private void OnRenderImage(RenderTexture src, RenderTexture dest) { if(oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", OldFilmEffectAmount); material.SetFloat("_Contrast", contrast); material.SetFloat("_cubicDistortion", cubicDistortion); material.SetFloat("_distortion", distortion); material.SetFloat("_scale",scale); if(vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if(scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); } if(dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_dustYSpeed", dustYSpeed); material.SetFloat("_dustXSpeed", dustXSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } 3.为了完成特效的脚本部分,接下来只要确保把变量值控制在合理的范围而不是任意值就可以了。 private void Update() { vignetteAmount = Mathf.Clamp01(vignetteAmount); OldFilmEffectAmount = Mathf.Clamp(OldFilmEffectAmount, 0f, 1.5f); randomValue = Random.Range(-1f,1f); contrast = Mathf.Clamp(contrast, 0f, 4f); distortion = Mathf.Clamp(distortion, -1f,1f); cubicDistortion = Mathf.Clamp(cubicDistortion, -1f, 1f); scale = Mathf.Clamp(scale, 0f, 1f); } 注意 作者这里只贴了关键代码,下面是完整的脚本代码[译者注]: using UnityEngine; [ExecuteInEditMode] public class OldFilmEffect : MonoBehaviour { #region Variables public Shader oldFilmShader; public float OldFilmEffectAmount = 1.0f; public float contrast = 3.0f; public float distortion = 0.2f; public float cubicDistortion = 0.6f; public float scale = 0.8f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; 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; private float randomValue; #endregion #region Properties Material material { get { if(curMaterial == null) { curMaterial = new Material(oldFilmShader); curMaterial.hideFlags = HideFlags.HideAndDontSave; } return curMaterial; } } #endregion private void Start() { if (!SystemInfo.supportsImageEffects) { enabled = false; return; } if (oldFilmShader && !oldFilmShader.isSupported) { enabled = false; } } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if(oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", OldFilmEffectAmount); material.SetFloat("_Contrast", contrast); material.SetFloat("_cubicDistortion", cubicDistortion); material.SetFloat("_distortion", distortion); material.SetFloat("_scale",scale); if(vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if(scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); } if(dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_dustYSpeed", dustYSpeed); material.SetFloat("_dustXSpeed", dustXSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(src, dest, material); } else { Graphics.Blit(src, dest); } } private void Update() { vignetteAmount = Mathf.Clamp01(vignetteAmount); OldFilmEffectAmount = Mathf.Clamp(OldFilmEffectAmount, 0f, 1.5f); randomValue = Random.Range(-1f,1f); contrast = Mathf.Clamp(contrast, 0f, 4f); distortion = Mathf.Clamp(distortion, -1f,1f); cubicDistortion = Mathf.Clamp(cubicDistortion, -1f, 1f); scale = Mathf.Clamp(scale, 0f, 1f); } private void OnDisable() { if (curMaterial) { DestroyImmediate(curMaterial); } } } 4.当我们的脚本完成之后,注意力转移到着色器上来。我们要在着色器上也创建跟脚本变量对应的着色器变量。这样的话可以让脚本跟着色器相互通信。在着色器的 属性块(Properties block) 中添加下面的代码: Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _VignetteTex ("Vignette Texture", 2D) = "white"{} _ScratchesTex ("Scartches 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 _dustXSpeed ("Dust X Speed", Float) = 10.0 _dustYSpeed ("Dust Y Speed", Float) = 10.0 _RandomValue ("Random Value", Float) = 1.0 _Contrast ("Contrast", Float) = 3.0 _distortion ("Distortion", Float) = 0.2 _cubicDistortion ("Cubic Distortion", Float) = 0.6 _scale ("Scale (Zoom)", Float) = 0.8 } 5.接下来,跟往常一样,接下来在 CGPROGRAM 块中添加与属性块对应的变量好让属性块可以跟 CGPROGRAM 块通信: uniform sampler2D _MainTex; uniform sampler2D _VignetteTex; uniform sampler2D _ScratchesTex; uniform sampler2D _DustTex; fixed4 _SepiaColor; fixed _VignetteAmount; fixed _ScratchesYSpeed; fixed _ScratchesXSpeed; fixed _dustXSpeed; fixed _dustYSpeed; fixed _EffectAmount; fixed _RandomValue; fixed _Contrast; float _distortion; float _cubicDistortion; float _scale; 6.现在,我们简单的将 frag() 函数内容修改一下好让我们能处理屏幕效果上的像素。让我们获取来自脚本的 渲染纹理(render texture) 和 渐晕纹理(vignette texture): fixed4 frag(v2f_img i) : COLOR { //获得渲染纹理的颜色并且获取 v2f_img的uv half2 distortedUV = barrelDistortion(i.uv); distortedUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005)); fixed4 renderTex = tex2D(_MainTex, i.uv); //获取渐晕纹理的像素 fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); 7.我们接下来添加处理划痕和灰尘的代码: //处理划痕的UV和像素 half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), i.uv.y + (_Time.x * _ScratchesYSpeed)); fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); //处理灰尘的UV和像素 half2 dustUV = half2(i.uv.x + (_RandomValue * (_SinTime.z * _dustXSpeed)), i.uv.y + (_RandomValue * (_SinTime.z * _dustYSpeed))); fixed4 dustTex = tex2D(_DustTex, dustUV); 8.接下来处理深褐色色调: //使用YIQ值从渲染纹理中获取亮度值。 fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb); //给亮度值加上一个常量颜色 fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + fixed4(0.1f, 0.1f, 0.1f, 1.0f), _RandomValue); finalColor = pow(finalColor, _Contrast); 9.最后,我们将所有的层和颜色都叠加到一块并将最终的屏幕效果纹理返回: //创建一个白色的常量颜色,这样我们可以调节效果的不透明度。 fixed3 constantWhite = fixed3(1,1,1); //将不同的层混合到一起来创建最终的屏幕效果 finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); finalColor.rgb *= lerp(scratchesTex, constantWhite, (_RandomValue)); finalColor.rgb *= lerp(dustTex.rgb, constantWhite, (_RandomValue * _SinTime.z)); finalColor = lerp(renderTex, finalColor, _EffectAmount); return finalColor; } 10.当我们把所有代码都完成并且没有遇到错误,你应该有个跟下图很相似的结果。在编辑器中点击运行按钮,就可以看到灰尘效果和划痕效果,而且可以看到屏幕效果上的轻微的图片位移:
注意 作者少说了一个函数,将下面的 barrelDistortion 函数添加到 frag() 函数上面[译者注]: float2 barrelDistortion(float2 coord) { // Inspired by SynthEyes lens distortion algorithm // See 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 + _cubicDistortion * sqrt(r2)); return f * _scale * h + 0.5; } 原理介绍
现在让我们来逐步分析这个屏幕效果的每一层,剖析每一行代码的工作原理,以及对于这个屏幕效果我们该如何丰富它这方面获得更多的简洁。
...