Shaders被广泛用于视频游戏中,用于产生如光照模型、扭曲、(运动)模糊、发光和边缘检测的效果。常用的Shaders有三种类型:顶点着色器、片段着色器和集合着色器,在Unity中用户可以编写自定义Shaders。
我们都知道,实时游戏的一个很重要的特性就是让玩家有种身临其境的感觉。越是现代的游戏,越使用了很多的画面特效来达到这种沉浸感。在本文中,我们将学习到一些常见的游戏画面特效,包括如何把一个正常的画面改成一个老电影式画面的效果、在多人射击游戏中如何在屏幕上应用夜视效果。
首先,我们来学习如何创建一个老电影式的画面特效。
游戏往往会建立在不同的背景时间上。一些发生在想象的世界中,一些发生在未来世界中,还有一些甚至发生在古老的西方,而那时候电影摄像机才刚刚发展起来,人们看到的电影都是黑白的,有时还会呈现出棕褐色调(a sepia effect,Unity Pro中有自带的脚本和Shader)的着色效果。这种效果看起来非常独特,我们将在Unity中使用画面特效来重现这种效果。
实现这个效果需要一些步骤。我们先来分析一下下面的图像,然后分解制作这样老电影视觉的步骤:

上面的图像实际是有一系列从网上找到的图片组合起来实现的。我们可以利用Photoshop来创建这样风格的图片,来帮助你完成画面特效的草图。进行这样的过程(在Photoshop里制作原型)不仅可以告诉我们需要哪些元素,还可以快速让我们知道应该使用哪些混合模式,以及如何构建屏幕特效的图层(layers)。不过作者说的Photoshop源文件我没有找到。
本文最后实现的效果大概就是下面这样啦:

而原始的画面是:

准备工作
现在让我们来看一下每一个图层是如何被组合在一起从而创建出最后的效果的,以便我们为Shader和脚本准备下所需的资源。
棕褐色调(Sepia Tone):这种效果是比较容易是新建的,我们只需要从原始的render texture中把所有像素颜色转换到一个单一的颜色范围即可。这可以通过使用原始图像的光度(luminance)加上一个常量颜色值来实现。我们第一个图层看起来像下面这样:

晕影效果(Vignette effect):我们总是可以看到,当使用老的电影投影机把老电影投影到屏幕上时,总有些模糊的边框。这是因为,老式投影仪使用的灯泡在中央的亮度高于四周的亮度。这种效果通常被称为晕影效果(Vignette Effect),而这正是我们屏幕特效的第二个图层。我们可以使用一张叠加的纹理覆盖在整个屏幕上来达到这种效果。下面的图像展示了这个图层单独看起来的样子:
灰尘(Dust)和划痕(Scratches):最后一层图层就是灰尘(Dust)和划痕(Scratches)了。这个图层利用了两张不同的平铺(tiled)纹理,一个用于灰尘,一个用于划痕。使用它们的原因是因为我们想要使用不同的平铺速率,按时间来移动这两张纹理。由于老电影的每一帧通常都会出现一些小的划痕和灰尘,这使得整个画面看起来像电影正在放映。下面的图片展示了这个图层单独看起来的效果:
上面是分析了Photoshop里面各图层的样子和实现。现在,我们来使用上述纹理在Unity里正式实现我们的画面特效!
准备好一张晕影(Vignette)纹理,一张灰层纹理,一张划痕纹理,你可以点击此处下载。
创建一个新的脚本,命名为OldFilmEffect.cs。创建一个新的Shader,命名为OldFilmEffectShader.shader。
使用以下代码填充上述新的脚本和Shader。
完整的TestRenderImage.cs代码如下:
02 |
using System.Collections; |
05 |
public class TestRenderImage : MonoBehaviour {
|
08 |
public Shader curShader;
|
09 |
public float grayScaleAmount = 1 .0f;
|
10 |
private Material curMaterial;
|
14 |
public Material material {
|
16 |
if (curMaterial == null ) {
|
17 |
curMaterial = new Material(curShader);
|
18 |
curMaterial.hideFlags = HideFlags.HideAndDontSave;
|
27 |
if (SystemInfo.supportsImageEffects == false ) {
|
32 |
if (curShader != null && curShader.isSupported == false ) {
|
37 |
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
|
38 |
if (curShader != null ) {
|
39 |
material.SetFloat( "_LuminosityAmount" , grayScaleAmount);
|
41 |
Graphics.Blit(sourceTexture, destTexture, material);
|
43 |
Graphics.Blit(sourceTexture, destTexture);
|
49 |
grayScaleAmount = Mathf.Clamp(grayScaleAmount, 0 .0f, 1 .0f);
|
53 |
if (curMaterial != null ) {
|
54 |
DestroyImmediate(curMaterial);
|
完整的ImageEffect.shader代码如下:
01 |
Shader "Custom/ImageEffect" {
|
03 |
_MainTex ( "Base (RGB)" , 2D) = "white" {}
|
04 |
_LuminosityAmount ( "GrayScale Amount" , Range( 0.0 , 1.0 )) = 1.0 |
09 |
#pragma vertex vert_img
|
12 |
#include "UnityCG.cginc" |
14 |
uniform sampler2D _MainTex;
|
15 |
fixed _LuminosityAmount;
|
17 |
fixed4 frag(v2f_img i) : COLOR
|
21 |
fixed4 renderTex = tex2D(_MainTex, i.uv);
|
24 |
float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b;
|
25 |
fixed4 finalColor = lerp(renderTex, luminosity, _LuminosityAmount);
|
把OldFilmEffect脚本添加到Camera上,并使用OldFilmEffectShader给OldFilmEffect脚本中的Cur Shader赋值。
实现
我们的老电影式的画面特效中的每一个独立图层实际都很简单,但是,当我们把它们整合在一起我们就可以得到非常震撼的效果。现在你的画面特效脚本系统应该已经建立好了,现在我们来实现具体的脚本和Shader。
首先,我们来填写脚本的主要代码。
1、第一步我们要定义一些需要在面板中显示的变量,以便让用户进行调整。我们可以利用之前制作原型所用的Photoshop作为参考,来决定我们需要显示哪些变量。在脚本中添加如下代码:
02 |
public Shader oldFilmShader;
|
04 |
public float oldFilmEffectAmount = 1 .0f;
|
06 |
public Color sepiaColor = Color.white;
|
07 |
public Texture2D vignetteTexture;
|
08 |
public float vignetteAmount = 1 .0f;
|
10 |
public Texture2D scratchesTexture;
|
11 |
public float scratchesXSpeed;
|
12 |
public float scratchesYSpeed;
|
14 |
public Texture2D dustTexture;
|
15 |
public float dustXSpeed;
|
16 |
public float dustYSpeed;
|
18 |
private Material curMaterial;
|
19 |
private float randomValue;
|
2、然后,我们需要填充OnRenderImage函数。在这个函数里,我们将要把上述变量传递给Shader,使得Shader可以使用这些数据来处理render texture:
01 |
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
|
02 |
if (oldFilmShader != null ) {
|
03 |
material.SetColor( "_SepiaColor" , sepiaColor);
|
04 |
material.SetFloat( "_VignetteAmount" , vignetteAmount);
|
05 |
material.SetFloat( "_EffectAmount" , oldFilmEffectAmount);
|
07 |
if (vignetteTexture) {
|
08 |
material.SetTexture( "_VignetteTex" , vignetteTexture);
|
11 |
if (scratchesTexture) {
|
12 |
material.SetTexture( "_ScratchesTex" , scratchesTexture);
|
13 |
material.SetFloat( "_ScratchesXSpeed" , scratchesXSpeed);
|
14 |
material.SetFloat( "_ScratchesYSpeed" , scratchesYSpeed);
|
18 |
material.SetTexture( "_DustTex" , dustTexture);
|
19 |
material.SetFloat( "_DustXSpeed" , dustXSpeed);
|
20 |
material.SetFloat( "_DustYSpeed" , dustYSpeed);
|
21 |
material.SetFloat( "_RandomValue" , randomValue);
|
24 |
Graphics.Blit(sourceTexture, destTexture, material);
|
26 |
Graphics.Blit(sourceTexture, destTexture);
|
3、最后,我们需要在Update函数中保证一些变量的范围:
2 |
vignetteAmount = Mathf.Clamp(vignetteAmount, 0 .0f, 1 .0f);
|
3 |
oldFilmEffectAmount = Mathf.Clamp(oldFilmEffectAmount, 0 .0f, 1 .0f);
|
4 |
randomValue = Random.Range(- 1 .0f, 1 .0f);
|
文章部分内容摘自DevStore,实现关键的Shader部分可查看http://www.devstore.cn/new/newInfo/761.html