本系列主要参考《Unity Shaders and Effects Cookbook》一书(感谢原书作者),同时会加上一点个人理解或拓展。
这里是本书所有的插图。这里是本书所需的代码和资源(当然你也可以从官网下载)。
========================================== 分割线 ==========================================
终于到了本书的最后一章了,好激动有木有!作为压轴章,虽然只有两篇,但每篇的内容是比之前的任何一篇都要复杂。写完这章要去总结一下啦~那么,开始学习吧!
学习这本书的人绝大部分在业余时间玩过一两个游戏。那么,你肯定有感触,实时游戏的一个很重要的特性就是要让玩家有种身临其境的感觉。越是现代的游戏,越是使用了更多的画面特效来达到这种沉浸感。
使用画面特效,只需通过改变游戏画面,我们就可以把某个环境的氛围烘托成冷静到恐怖各个层次。想象我们在某个关卡中走入一个房间,然后游戏接管,开始播放过场动画。许多现代游戏都是用了不同的画面特效来改变当前时刻的气氛。而理解怎样创建这些在游戏性中被触发的画面特效将是我们下面要完成的工作。
在本章中,我们将会学习一些常见的游戏画面特效。这些包括,如何把一个正常的画面改变成一个老电影式的画面效果,在许多第一人称射击游戏中如何在屏幕上应用夜视效果(night vision effects)。
首先,我们来学习如何创建一个老电影式的画面特效。
游戏往往会建立在不同的背景时间上。一些发生在想象的世界中,一些发生在未来世界中,还有一些甚至发生在古老的西方,而那时候电影摄像机才刚刚发展起来,人们看到的电影都是黑白的,有时还会呈现出棕褐色调(a sepia effect,Unity Pro中有自带的脚本和Shader)的着色效果。这种效果看起来非常独特,我们将在Unity中使用画面特效来重现这种效果。
实现这个效果需要一些步骤。我们先来分析一下下面的图像,然后分解制作这样老电影视觉的步骤:
上面的图像实际是有一系列从网上找到的图片组合起来实现的。我们可以利用Photoshop来创建这样风格的图片,来帮助你完成画面特效的草图。进行这样的过程(在Photoshop里制作原型)不仅可以告诉我们需要哪些元素,还可以快速让我们知道应该使用哪些混合模式,以及如何构建屏幕特效的图层(layers)。不过作者说的Photoshop源文件我没有找到。。。
本文最后实现的效果大概就是下面这样啦:
而原始的画面是:
现在让我们来看一下每一个图层是如何被组合在一起从而创建出最后的效果的,以便我们为Shader和脚本准备下所需的资源。
上面是分析了Photoshop里面各图层的样子和实现。现在,我们来使用上述纹理在Unity里正式实现我们的画面特效!
#region Variables public Shader oldFilmShader; public float oldFilmEffectAmount = 1.0f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; public float vignetteAmount = 1.0f; public Texture2D scratchesTexture; public float scratchesXSpeed; public float scratchesYSpeed; public Texture2D dustTexture; public float dustXSpeed; public float dustYSpeed; private Material curMaterial; private float randomValue; #endregion
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){ if (oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", oldFilmEffectAmount); if (vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if (scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); } if (dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_DustXSpeed", dustXSpeed); material.SetFloat("_DustYSpeed", dustYSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(sourceTexture, destTexture, material); } else { Graphics.Blit(sourceTexture, destTexture); } }
void Update () { vignetteAmount = Mathf.Clamp(vignetteAmount, 0.0f, 1.0f); oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0.0f, 1.0f); randomValue = Random.Range(-1.0f, 1.0f); }
接下来,我们来实现关键的Shader部分。
Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _VignetteTex ("Vignette Texture", 2D) = "white" {} _VignetteAmount ("Vignette Opacity", Range(0, 1)) = 1 _ScratchesTex ("Scraches Texture", 2D) = "white" {} _ScratchesXSpeed ("Scraches X Speed", Float) = 10.0 _ScratchesYSpeed ("Scraches Y Speed", Float) = 10.0 _DustTex ("Dust Texture", 2D) = "white" {} _DustXSpeed ("Dust X Speed", Float) = 10.0 _DustYSpeed ("Dust Y Speed", Float) = 10.0 _SepiaColor ("Sepia Color", Color) = (1, 1, 1, 1) _EffectAmount ("Old Film Effect Amount", Range(0, 1)) = 1 _RandomValue ("Random Value", Float) = 1.0 }
SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform sampler2D _VignetteTex; uniform sampler2D _ScratchesTex; uniform sampler2D _DustTex; fixed4 _SepiaColor; fixed _VignetteAmount; fixed _ScratchesXSpeed; fixed _ScratchesYSpeed; fixed _DustXSpeed; fixed _DustYSpeed; fixed _EffectAmount; fixed _RandomValue;
fixed4 frag (v2f_img i) : COLOR { half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005)); fixed4 renderTex = tex2D(_MainTex, renderTexUV); // Get teh pixed from the Vignette Texture fixed4 vignetteTex = tex2D(_VignetteTex, i.uv);
// Process the Scratches UV and pixels half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), i.uv.y + (_Time.x * _ScratchesYSpeed)); fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); // Process the Dust UV and pixels half2 dustUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _DustXSpeed), i.uv.y + (_Time.x * _DustYSpeed)); fixed4 dustTex = tex2D(_DustTex, dustUV);
// Get the luminosity values from the render texture using the YIQ values fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb); // Add the constant calor to the lum values fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + fixed4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue);
// Create a constant white color we can use to adjust opacity of effects fixed3 constantWhite = fixed3(1, 1, 1); // Composite together the different layers to create final Screen Effect finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); finalColor.rgb *= lerp(scratchesTex, constantWhite, _RandomValue); finalColor.rgb *= lerp(dustTex, constantWhite, (_RandomValue * _SinTime.z)); finalColor = lerp(renderTex, finalColor, _EffectAmount); return finalColor;
using UnityEngine; using System.Collections; [ExecuteInEditMode] public class OldFilmEffect : MonoBehaviour { #region Variables public Shader oldFilmShader; public float oldFilmEffectAmount = 1.0f; public Color sepiaColor = Color.white; public Texture2D vignetteTexture; public float vignetteAmount = 1.0f; public Texture2D scratchesTexture; public float scratchesXSpeed; public float scratchesYSpeed; public Texture2D dustTexture; public float dustXSpeed; public float dustYSpeed; private Material curMaterial; private float randomValue; #endregion #region Properties public Material material { get { if (curMaterial == null) { curMaterial = new Material(oldFilmShader); curMaterial.hideFlags = HideFlags.HideAndDontSave; } return curMaterial; } } #endregion // Use this for initialization void Start () { if (SystemInfo.supportsImageEffects == false) { enabled = false; return; } if (oldFilmShader != null && oldFilmShader.isSupported == false) { enabled = false; } } void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){ if (oldFilmShader != null) { material.SetColor("_SepiaColor", sepiaColor); material.SetFloat("_VignetteAmount", vignetteAmount); material.SetFloat("_EffectAmount", oldFilmEffectAmount); if (vignetteTexture) { material.SetTexture("_VignetteTex", vignetteTexture); } if (scratchesTexture) { material.SetTexture("_ScratchesTex", scratchesTexture); material.SetFloat("_ScratchesXSpeed", scratchesXSpeed); material.SetFloat("_ScratchesYSpeed", scratchesYSpeed); } if (dustTexture) { material.SetTexture("_DustTex", dustTexture); material.SetFloat("_DustXSpeed", dustXSpeed); material.SetFloat("_DustYSpeed", dustYSpeed); material.SetFloat("_RandomValue", randomValue); } Graphics.Blit(sourceTexture, destTexture, material); } else { Graphics.Blit(sourceTexture, destTexture); } } // Update is called once per frame void Update () { vignetteAmount = Mathf.Clamp(vignetteAmount, 0.0f, 1.0f); oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0.0f, 1.0f); randomValue = Random.Range(-1.0f, 1.0f); } void OnDisable () { if (curMaterial != null) { DestroyImmediate(curMaterial); } } }
Shader "Custom/OldFilmEffectShader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _VignetteTex ("Vignette Texture", 2D) = "white" {} _VignetteAmount ("Vignette Opacity", Range(0, 1)) = 1 _ScratchesTex ("Scraches Texture", 2D) = "white" {} _ScratchesXSpeed ("Scraches X Speed", Float) = 10.0 _ScratchesYSpeed ("Scraches Y Speed", Float) = 10.0 _DustTex ("Dust Texture", 2D) = "white" {} _DustXSpeed ("Dust X Speed", Float) = 10.0 _DustYSpeed ("Dust Y Speed", Float) = 10.0 _SepiaColor ("Sepia Color", Color) = (1, 1, 1, 1) _EffectAmount ("Old Film Effect Amount", Range(0, 1)) = 1 _RandomValue ("Random Value", Float) = 1.0 } SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include "UnityCG.cginc" uniform sampler2D _MainTex; uniform sampler2D _VignetteTex; uniform sampler2D _ScratchesTex; uniform sampler2D _DustTex; fixed4 _SepiaColor; fixed _VignetteAmount; fixed _ScratchesXSpeed; fixed _ScratchesYSpeed; fixed _DustXSpeed; fixed _DustYSpeed; fixed _EffectAmount; fixed _RandomValue; fixed4 frag (v2f_img i) : COLOR { half2 renderTexUV = half2(i.uv.x, i.uv.y + (_RandomValue * _SinTime.z * 0.005)); fixed4 renderTex = tex2D(_MainTex, renderTexUV); // Get teh pixed from the Vignette Texture fixed4 vignetteTex = tex2D(_VignetteTex, i.uv); // Process the Scratches UV and pixels half2 scratchesUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _ScratchesXSpeed), i.uv.y + (_Time.x * _ScratchesYSpeed)); fixed4 scratchesTex = tex2D(_ScratchesTex, scratchesUV); // Process the Dust UV and pixels half2 dustUV = half2(i.uv.x + (_RandomValue * _SinTime.z * _DustXSpeed), i.uv.y + (_Time.x * _DustYSpeed)); fixed4 dustTex = tex2D(_DustTex, dustUV); // Get the luminosity values from the render texture using the YIQ values fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb); // Add the constant calor to the lum values fixed4 finalColor = lum + lerp(_SepiaColor, _SepiaColor + fixed4(0.1f, 0.1f, 0.1f, 0.1f), _RandomValue); // Create a constant white color we can use to adjust opacity of effects fixed3 constantWhite = fixed3(1, 1, 1); // Composite together the different layers to create final Screen Effect finalColor = lerp(finalColor, finalColor * vignetteTex, _VignetteAmount); finalColor.rgb *= lerp(scratchesTex, constantWhite, _RandomValue); finalColor.rgb *= lerp(dustTex, constantWhite, (_RandomValue * _SinTime.z)); finalColor = lerp(renderTex, finalColor, _EffectAmount); return finalColor; } ENDCG } } FallBack "Diffuse" }