【Unity Shaders】游戏性和画面特效——创建一个夜视效果的画面特效

本系列主要参考《Unity Shaders and Effects Cookbook》一书(感谢原书作者),同时会加上一点个人理解或拓展。

这里是本书所有的插图。这里是本书所需的代码和资源(当然你也可以从官网下载)。

========================================== 分割线 ==========================================

写在前面

呜呼,这本书最后一篇了有木有!!!想想就有点小激动呢。。。(可是还有很多隔过去了。)技术文章里就不说太多废话了,之后会写一篇总结的,坑也会慢慢补上的(我希望吧。)。

好啦!我们本节要学习的特效是一个更加常见的画面特效。在市场上很多的第一人称射击游戏(FPS)中,我们都可以看到夜视效果的身影,比如《使命召唤》、《光晕》等等。这种效果使屏幕蒙上一层非常独特的黄绿色。下面是《使命召唤》中的夜视效果:

 

为了实现这种夜视效果,我们需要使用Photoshop来分解上述效果的实现。我们可以从网上找一些参考图片,然后和已有图层合并,看看我们需要哪种混合效果,以及我们要怎样组合不同的图层。下面的图像显示了在Photoshop中进行上述过程的最后结果:

而最后Shader实现的结果像下面这样:

下面,我们开始将Photoshop中的最终图像分解成几个部分,来更好的理解这些资源是如何被整合在一起的。而后,我们会深入讲解实现过程。

准备工作

之前说过,使用Photoshop可以让我们方便地构分层图像,以便我们可以更好地理解如何得到一个夜视效果。现在,我们就把夜视效果分解成几个组成图层。

  • 偏绿色的画面(Tinted green):夜视效果的第一个图层就是它标志性的绿色,几乎所有的夜视图像都是这种色调的。这可以让我们的特效看起来就是明显的夜视效果。

  • 扫描线(Scan lines):为了增加夜视效果的真实性,我们在上述着色层的上面添加了一层扫描线。在这里,我们使用了一张用Photoshop创建的纹理,然后让用户选择平铺系数来控制扫描线的宽窄和大小。

  • 噪点(Noise):下一个图层是一个简单的噪点图层,它平铺在上述两种图层的上方,用于分解图像为我们的特效添加更多的细节效果。

  • 晕影(Vignette):夜视效果的最后一层就是晕影。从上面《使命召唤》的游戏截图可以看出,它使用一个晕影来模拟一个视野效果。我们使用下面的图层来作为晕影纹理。

现在,我们把上述各个纹理结合在一起,开始正式创建夜视效果的画面特效。

  1. 准备好一张晕影纹理、一张噪声纹理和一张扫描线纹理,你可以在本书资源(见本文最上方)中找到对应的纹理。
  2. 创建一个新的脚本,命名为NightVisionEffect.cs,以及一个新的Shader,命名为NightVisionEffectShader.shader。
  3. 使用前一章第一篇里的代码填充上述新的脚本和Shader。
  4. 把OldFilmEffect脚本添加到Camera上,并使用OldFilmEffectShader给OldFilmEffect脚本中的Cur Shader赋值。

实现

现在你的画面特效脚本系统应该已经建立好了,现在我们来实现具体的脚本和Shader。

首先,我们来填写NightVisionEffect.cs脚本的主要代码。

  1. 第一步我们要定义一些需要在面板中显示的变量,以便让用户进行调整。在脚本中添加如下代码:

    	#region Variables
    	public Shader nightVisionShader;
    
    	public float contrast = 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 = 0.0f;
    	#endregion
  2. 下面,我们需要填充OnRenderImage函数。在这个函数里,我们将要把上述变量传递给Shader,使得Shader可以使用这些数据来处理render texture:
    	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 (vignetteTexture) {
    				material.SetTexture("_VignetteTex", vignetteTexture);
    			}
    
    			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. 最后,我们需要保证一些变量的范围。这些范围可以根据需要后续更改:
    	void Update () {
    		contrast = Mathf.Clamp(contrast, 0.0f, 4.0f);
    		brightness = Mathf.Clamp(brightness, 0.0f, 2.0f);
    		distortion = Mathf.Clamp(distortion, -1.0f, 1.0f);
    		scale = Mathf.Clamp(scale, 0.0f, 3.0f);
    		randomValue = Random.Range(-1.0f, 1.0f);
    	}

接下来,我们来实现关键的Shader部分。

  1. 首先,还是添加一些新的Properties:

    	Properties {
    		_MainTex ("Base (RGB)", 2D) = "white" {}
    		_VignetteTex ("Vignette Texture", 2D) = "white" {}
    		_ScanLineTex ("Scan Line Texture", 2D) = "white" {}
    		_ScanLineTileAmount ("Scale Line Tile Amount", Float) = 4.0
    		_NoiseTex ("Noise Texture", 2D) = "white" {}
    		_NoiseXSpeed ("Noise X Speed", Float) = 100.0
    		_NoiseYSpeed ("Noise Y Speed", Float) = 100.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
    	}
  2. 我们还需要在CGPROGRAM块中添加对应的变量,以便Properties块可以和CGPROGRAM块通信:
    	SubShader {
    		Pass {
    			CGPROGRAM
    
    			#pragma vertex vert_img
    			#pragma fragment frag
    
    			#include "UnityCG.cginc"
    
    			uniform sampler2D _MainTex;
    			uniform sampler2D _VignetteTex;
    			uniform sampler2D _ScanLineTex;
    			fixed _ScanLineTileAmount;
    			uniform sampler2D _NoiseTex;
    			fixed _NoiseXSpeed;
    			fixed _NoiseYSpeed;
    			fixed4 _NightVisionColor;
    			fixed _Contrast;
    			fixed _Brightness;
    			fixed _RandomValue;
    			fixed _Distortion;
    			fixed _Scale;
  3. 我们的特效还需要一个透镜变形(lens distortion)效果,来模拟从透镜中观察、图像边界由于透镜度数而发生变形的效果。在变量声明的下方添加如下代码:
    			float2 barrelDistortion(float2 coord) {
    				// 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 * sqrt(r2));
    
    				return f * _Scale * h + 0.5;
    			}

    解释:夜视效果实际和之前的老电影效果很像,都是把一些图层组合、模块化起来。最大的不同就是这里使用了一个透镜变形效果。

    上述算法是由SynthEyes的成员提供的,免费使用哦~第一行代码首先找到纹理的中心——float(0.5, 0.5)。一旦得到了图像中心,我们可以根据像素距离中心的远近对像素应用一个拉伸。具体分析可见本文最下方的相关链接。

  4. 现在,我们到了整个Shader的关键部分。首先,在frag函数里添加如下代码来得到render texture和晕影纹理:
    			fixed4 frag(v2f_img i) : COLOR {
    				// Get the colors from the Render Texture and the uv‘s
    				// from the v2f_img struct
    				half2 distortedUV = barrelDistortion(i.uv);
    				fixed4 renderTex = tex2D(_MainTex, distortedUV);
    				fixed4 vignetteTex = tex2D(_VignetteTex, i.uv);

    解释:这三行代码很简单。在得到了变形后的UV坐标后,采样得到render texture中对应的像素,再按正常的UV得到晕影纹理的对应像素。

    最后,得到了底层图像像素renderTex和晕影图层的像素vignetteTex。

  5. 下一步,我们需要处理扫描线和噪点纹理,通过UV坐标为它们添加合适的动画:
    				// Process  scan lines and noise
    				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);

    解释:这里跟之前老电影的纹理动画处理方式类似。

    对于扫描线纹理,之前我们说过,通过_ScanLineTileAmount可以调整画面上扫描线的宽窄和多少。_ScanLineTileAmount越大,条纹越密越细,反之越宽。

    对于噪点纹理,我们需要添加一个动态效果。在XY方向上处理方式稍有不同,这是为什么呢?我自己的理解是这样的。。。对于X方向,我们想要模拟的是一个小幅度的类似抖动的效果,因此用_SinTime作为因子。而对于Y方向,我们想要模拟向一个方向不断滚动的效果,因此使用_Time作为因子。

    最后,根据上述UV坐标可以得到扫描线纹理的像素scanLineTex和噪点纹理的像素noiseTex。

  6. 最后一层图层是偏绿的颜色效果。我们仅仅需要处理render texture的光度值(Luminance),然后给它添加一个夜视颜色,来得到最终形象的夜视效果:
    				// Get the luminosity values from the render texture using the YIQ values
    				fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb);
    				lum += _Brightness;
    				fixed4 finalColor = (lum * 2) + _NightVisionColor;

    解释:最后一层图层是为整个画面添加绿色色调。首先也是通过YIQ得到当前的光度值lum,再加上_Brightness来调整亮度。最后,再加上_NightVisionColor(也就是绿色)。这里lum*2是为了不至于让整个画面太暗。

  7. 最后,我们把所有的图层结合在一起,返回最终的像素值:
    				// Final output
    				finalColor = pow(finalColor, _Contrast);
    				finalColor *= vignetteTex;
    				finalColor *= scanLineTex * noiseTex;
    
    				return finalColor;
    			}

完整的脚本和Shader如下:

NightVisionEffect脚本:

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class NightVisionEffect : MonoBehaviour {

	#region Variables
	public Shader nightVisionShader;

	public float contrast = 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 = 0.0f;
	#endregion

	#region Properties
	public Material material {
		get {
			if (curMaterial == null) {
				curMaterial = new Material(nightVisionShader);
				curMaterial.hideFlags = HideFlags.HideAndDontSave;
			}
			return curMaterial;
		}
	}
	#endregion

	// Use this for initialization
	void Start () {
		if (SystemInfo.supportsImageEffects == false) {
			enabled = false;
			return;
		}

		if (nightVisionShader != null && nightVisionShader.isSupported == false) {
			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 (vignetteTexture) {
				material.SetTexture("_VignetteTex", vignetteTexture);
			}

			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 frame
	void Update () {
		contrast = Mathf.Clamp(contrast, 0.0f, 4.0f);
		brightness = Mathf.Clamp(brightness, 0.0f, 2.0f);
		distortion = Mathf.Clamp(distortion, -1.0f, 1.0f);
		scale = Mathf.Clamp(scale, 0.0f, 3.0f);
		randomValue = Random.Range(-1.0f, 1.0f);
	}

	void OnDisable () {
		if (curMaterial != null) {
			DestroyImmediate(curMaterial);
		}
	}
}

NightVisionEffectShader如下:

Shader "Custom/NightVisionEffectShader" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_VignetteTex ("Vignette Texture", 2D) = "white" {}
		_ScanLineTex ("Scan Line Texture", 2D) = "white" {}
		_ScanLineTileAmount ("Scale Line Tile Amount", Float) = 4.0
		_NoiseTex ("Noise Texture", 2D) = "white" {}
		_NoiseXSpeed ("Noise X Speed", Float) = 100.0
		_NoiseYSpeed ("Noise Y Speed", Float) = 100.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
	}
	SubShader {
		Pass {
			CGPROGRAM

			#pragma vertex vert_img
			#pragma fragment frag

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			uniform sampler2D _VignetteTex;
			uniform sampler2D _ScanLineTex;
			fixed _ScanLineTileAmount;
			uniform sampler2D _NoiseTex;
			fixed _NoiseXSpeed;
			fixed _NoiseYSpeed;
			fixed4 _NightVisionColor;
			fixed _Contrast;
			fixed _Brightness;
			fixed _RandomValue;
			fixed _Distortion;
			fixed _Scale;

			float2 barrelDistortion(float2 coord) {
				// 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 * sqrt(r2));

				return f * _Scale * h + 0.5;
			}

			fixed4 frag(v2f_img i) : COLOR {
				// Get the colors from the Render Texture and the uv‘s
				// from the v2f_img struct
				half2 distortedUV = barrelDistortion(i.uv);
				fixed4 renderTex = tex2D(_MainTex, distortedUV);
				fixed4 vignetteTex = tex2D(_VignetteTex, i.uv);

				// Process  scan lines and noise
				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);

				// Get the luminosity values from the render texture using the YIQ values
				fixed lum = dot(fixed3(0.299, 0.587, 0.114), renderTex.rgb);
				lum += _Brightness;
				fixed4 finalColor = (lum * 2) + _NightVisionColor;

				// Final output
				finalColor = pow(finalColor, _Contrast);
				finalColor *= vignetteTex;
				finalColor *= scanLineTex * noiseTex;

				return finalColor;
			}

			ENDCG
		}
	}
	FallBack "Diffuse"
}

完成后返回Unity编辑器查看效果。我们需要在面板中设置对应的图片和属性,像下面这样:

扩展链接

关于透镜形变效果:

时间: 2024-11-05 20:43:32

【Unity Shaders】游戏性和画面特效——创建一个夜视效果的画面特效的相关文章

【Unity Shaders】游戏性和画面特效——创建一个老电影式的画面特效

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 终于到了本书的最后一章了,好激动有木有!作为压轴章,虽然只有两篇,但每篇的内容是比之前的任

【Unity Shaders】Transparency —— 使用alpha通道创建透明效果

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 从这篇开始是一个全新的章节:透明效果(Transparency).之前在制作LOGO闪光效

【Unity Shaders】ShadowGun系列之一——飞机坠毁的浓烟效果

写在前面 最近一直在思考下面的学习该怎么进行,当然自己有在一边做项目一边学OpenGL,偶尔翻翻论文之类的.但是,写shader是一个需要实战和动手经验的过程,而模仿是前期学习的必经之路.很多人都会问,怎么学shader,看什么书.当然我经验也不够,目前的路线是:掌握一门着色语言+读几本经典书籍+学习优秀的shader实例+动手实践+动手实践+动手实践.每一个都不容易,所以学shader是一个漫长而艰辛的过程. 当当当~所以,在继Surface Shader系列之后,我打算学习一下现在已有的各种

【Unity Shaders】Lighting Models —— 光照模型之Lit Sphere

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 照亮的球体(Lit Sphere,翻译过来很怪)类型的光照模型是一种非常有趣的基于图像的光

【Unity Shaders】Transparency —— 使用渲染队列进行深度排序

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 为了让我们真正明白透明度,我们需要了解一下深度排序,或者说,对象的绘制顺序.Unity允许

Unity 提取游戏资源之ktx转换

从雨松的博文<Unity3D研究院之mac上从.ipa中提取unity3D游戏资源(六十六)>可以学到提取Unity的游戏资源,其中有用到一个工具:PVRTexTool 因为这个工具的官网不好下载,故将其上传到CSDN,下载地址:http://download.csdn.net/detail/akof1314/7660209 提取出来的资源,其中的ktx资源,用PVRTexToolGUI.exe可以打开查看,可以发现都是上下颠倒,且被拉伸 直接转为png格式的批处理脚本为: 1 2 3 4 5

Quick-cocos2d-x3.3 Study (十二)--------- 创建一个圆形的物体并带有物理引擎效果

创建带物理效果的心心图片 先创建一个物理场景 1 local GameScene = class("GameScene", function ( ) 2 -- body 3 -- return display.newScene("GameScene") 4 -- 创建一个带物理效果的场景 5 return display.newPhysicsScene("GameScene") 6 end) 在这个物理世界中添加物理心心 1 -- 获取场景中绑定

【Unity Shaders】使用Unity Render Textures实现画面特效——画面特效中的叠加(Overlay)混合模式

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 在这篇里,我们将会学习另一种混合模式,叠加(Overlay)混合模式.这种混合模式使用了条

【Unity Shaders】Reflecting Your World —— 在Unity3D中创建一个简单的动态Cubemap系统

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 我们已经学了很多关于反射的内容,但是我们现在的反射并不能实时反射,即当反射物体移动时它们不