使用Unity Render Textures实现画面特效——建立画面特效脚本系统

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

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

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

写在前面

最近由于成为研一新生,被入学的各种事情耽误,好久没有更新博客,好惭愧。。。刚收拾好我就来更新博客啦~快表扬我一下。。。前几天有好心人给我提建议说代码和解释分开容易导致学习不连贯,其实我也有这个感觉。我我为什么不写到注释里呢?一个是因为解释的内容比较多,注释里面说不太清楚;更重要的原因是,我的Mono不支持中文输入。。。只能复制粘贴!如果有人知道Mac的Mono里面怎么输入中文可以告诉我。。。为了让学习过程更连贯,以后的“解释”部分大部分都会移到“实现”中每一步的解释里面!

这节是一个新的章节,倒数第二章喽~Render Texture是一个非常好用的东西,通过本章我们就来学习如何用它来实现一些画面特效,像老电影效果、雪花效果等等。

先来介绍一下本章。学习编写Shader一个很有用的地方就是可以创建各种自定义的画面特效,也被称为后期特效(post effects)。使用这些画面特效,我们可以创建很多美妙的实时图像,例如高光(Bloom),运动模糊(Motion Blur),HDR特效(HDR effects)等等。大多数市面上的现代游戏使用了很多这样的画面特效,以此来得到景深效果,高光效果,甚至是进行颜色矫正。

通过本章,我们将学习如何建立一个脚本系统,来控制创建这些画面特效。我们会学习什么是render texture,什么是深度缓冲,以及怎样像Photoshop那样创建画面特效。通过这些内容,你不仅可以进一步加深Shader知识,还可以自己动脑在Unity中创建很棒的实时渲染。

那么怎样创建画面特效呢?

一般,我们会抓取一个完整的画面图像(或纹理),使用Shader在GPU上处理它的像素后,再返回给Unity的渲染器渲染到屏幕上,也就是一个后期处理的过程。这允许我们可以对渲染后的游戏图像进行实时地逐像素操作,从而给了我们一个全局的控制能力。

我们想象一个场景。我们需要调整游戏最后的画面对比度。如果我们去调整每个材质,虽然的确可能做到,但这会非常麻烦。而通过利用画面特效,我们就可以整体调整画面最后的效果。

为了让我们的画面特效系统能够建立并正常运行,我们需要创建一个单独的脚本来作为游戏当前已渲染的图像(也就是Unity的render texture)的通信员。这个脚本会把当前的render texture传递给Shader。

我们第一个画面特效是一个非常简单的灰度效果。那,开始吧!

准备工作

  1. 创建一个新的场景,创建一个平行光。
  2. 创建一个新的C#脚本,命名为TestRenderImage.cs。
  3. 创建一个新的Shader和Material,命名为ImageEffect。
  4. 创建一个球体,并把新材质赋给它。这个新的材质可以使用任何Shader,这里我们使用一个简单的红色高光反射材质,Specular Material。

最后,你的场景大概是这个样子的:

实现

我们总共需要一个脚本和一个Shader。

脚本:负责在编辑中实时更新图像,同时也负责从主摄像机抓取render texture,然后把该texture传递给Shader。

Shader:一旦render texture到达Shader后,我们就在这里面进行逐像素操作。

首先,我们来实现C#脚本。

  1. 为了让我们在Unity非运行时刻,也可以实时编辑和查看画面效果,我们需要在TestRenderImage类的上面声明下面的语句:

    [ExecuteInEditMode]
    public class TestRenderImage : MonoBehaviour {

    解释:ExecuteInEditMode的文档在这里。通常,一个脚本仅会在play模式下运行。而添加了这个声明后,我们的脚本就可以进行一些回调函数,即使Unity并不是在play模式下。而我们常见的一些回调函数的回调方式也有所不同:
    Update:在scene发生变化时被调用
    OnGUI:在Game视图接受到Event时被调用
    OnRenderImage:在Scene视图或Game视图重绘时被调用

  2. 打开TestRenderImage.cs脚本。首先我们添加一些新的变量:
    public class TestRenderImage : MonoBehaviour {
    
    	#region Variables
    	public Shader curShader;
    	public float grayScaleAmount = 1.0f;
    	private Material curMaterial;
    	#endregion

    解释:这里面的#region没有实际作用,仅仅是方面coder阅读源码~

  3. 因为我们的画面特效要使用Shader来执行对画面图像的像素操作,所以我们需要创建一个材质来运行Shader。如果没有材质,我们就不可以访问Shader的属性。因此,我们在C#中创建一个属性来检查是否有这样一个材质,如果没有就创建一个。
    	#region Properties
    	public Material material {
    		get {
    			if (curMaterial == null) {
    				curMaterial = new Material(curShader);
    				curMaterial.hideFlags = HideFlags.HideAndDontSave;
    			}
    			return curMaterial;
    		}
    	}
    	#endregion
  4. 现在,我们需要建立一些检查,来看看当前的游戏平台是否支持画面特效。如果不支持,我们在脚本的一开始就disable它:
    	// Use this for initialization
    	void Start () {
    		if (SystemInfo.supportsImageEffects == false) {
    			enabled = false;
    			return;
    		}
    
    		if (curShader != null && curShader.isSupported == false) {
    			enabled = false;
    		}
    	}
  5. 为了真正从Unity渲染器中抓取被渲染过的图像,我们需要使用Unity内置的OnRenderImage函数。下面的代码允许我们访问当前被渲染的图像:
    	void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
    		if (curShader != null) {
    			material.SetFloat("_LuminosityAmount", grayScaleAmount);
    
    			Graphics.Blit(sourceTexture, destTexture, material);
    		} else {
    			Graphics.Blit(sourceTexture, destTexture);
    		}
    	}

    解释:一旦通过上述的各种检查,我们就需要调用内置的OnRenderImage函数来实现画面特效。这个函数负责从Unity渲染器中抓取当前的render texture,然后使用Graphics.Blit()函数再传递给Shader(通过sourceTexture参数),然后再返回一个处理后的图像再次传递回给Unity渲染器(通过destTexture参数)。这两个行数互相搭配是处理画面特效的很常见的方法。你可以在下面的连接中找到这两个函数更详细的信息:
    OnRenderImage:该摄像机上的任何脚本都可以收到这个回调(意味着你必须把它附到一个Camera上)。允许你修改最后的Render Texture。
    Graphics.Blit:sourceTexture会成为material的_MainTex。

  6. 我们的画面效果需要一个名为的变量,它是用于控制最终屏幕效果的灰度值的。因此,我们需要保证这个值的范围在0到1,0表示没有任何灰度效果,1表示完全灰度化。我们在Update函数里执行这个操作:
    	// Update is called once per frame
    	void Update () {
    		grayScaleAmount = Mathf.Clamp(grayScaleAmount, 0.0f, 1.0f);
    	}
  7. 最后,由于我们在一开始创建了一个虚拟材质,我们需要在脚本被disable时,销毁该对象:
    	void OnDisable () {
    		if (curMaterial != null) {
    			DestroyImmediate(curMaterial);
    		}
    	}

完整的TestRenderImage.cs代码如下:

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class TestRenderImage : MonoBehaviour {

	#region Variables
	public Shader curShader;
	public float grayScaleAmount = 1.0f;
	private Material curMaterial;
	#endregion

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

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

		if (curShader != null && curShader.isSupported == false) {
			enabled = false;
		}
	}

	void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture){
		if (curShader != null) {
			material.SetFloat("_LuminosityAmount", grayScaleAmount);

			Graphics.Blit(sourceTexture, destTexture, material);
		} else {
			Graphics.Blit(sourceTexture, destTexture);
		}
	}

	// Update is called once per frame
	void Update () {
		grayScaleAmount = Mathf.Clamp(grayScaleAmount, 0.0f, 1.0f);
	}

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

现在,我们可以把该脚本赋给当前的主摄像机。你将会看到面板上出现一个grayScaleAmount值以及一个为Shader赋值的区域。现在运行的话,你不会看到任何变化,我们还差一个Shader啦~

继续Shader的编写。

  1. 首先还是需要一些新的属性:

    	Properties {
    		_MainTex ("Base (RGB)", 2D) = "white" {}
    		_LuminosityAmount ("GrayScale Amount", Range(0.0, 1.0)) = 1.0
    	}

    解释:之前提到,通过Graphics.Blit()函数,我们可以把抓取获得的Render Texture作为该材质的_MainTex属性传递给Shader。

  2. 这次的Shader需要利用纯正的Cg Shader代码,而不是Unity内置的Surface Shader。这会使得我们更加优化画面特效,因为我们仅仅需要去计算render texture的像素。因此,我们创建一个新的pass,并使用一些新的#pragma声明:
    	SubShader {
    		Pass {
    			CGPROGRAM
    			#pragma vertex vert_img
    			#pragma fragment frag
    
    			#include "UnityCG.cginc"

    解释:这涉及到了Vertex&Fragment Shader的知识。呜,后面补一篇入门级文章好了。这里简单解释一下。Vertex&Fragment Shader如它的名字一样,需要我们实现两个函数:vertex和fragment。vertex函数负责把模型顶点位置转换到摄像机的透视坐标系中,以及处理模型各顶点对应的纹理坐标等。fragment则负责输出最后屏幕每个像素的颜色值。这里,第一个#pragma指明我们使用名为vet_img的vertex函数,这个函数Unity为我们写好了,在UnityCG.cginc里,它就是完成了两个最简单的计算,计算顶点对应的透视坐标以及纹理坐标。第二个#pragma指明我们使用名为frag的fragment函数,这是在下面的步骤里实现的。

  3. 为了访问新的属性,我们需要在CGPROGRAM里创建对应的变量:
    			uniform sampler2D _MainTex;
    			fixed _LuminosityAmount;
  4. 最后,我们仅仅需要编写自己的像素函数,也就是这里的frag()函数。这是画面特效中真正改变画面像素值的地方。这个函数将会处理render texture的每个像素,然后向TestRenderImage.cs返回一个新的图像:
    			fixed4 frag(v2f_img i) : COLOR
    			{
    				//Get the colors from the RenderTexture and the uv‘s
    				//from the v2f_img struct
    				fixed4 renderTex = tex2D(_MainTex, i.uv);
    
    				//Apply the Luminosity values to our render texture
    				float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b;
    				fixed4 finalColor = lerp(renderTex, luminosity, _LuminosityAmount);
    
    				return finalColor;
    			}

完整的ImageEffect.shader代码如下:

Shader "Custom/ImageEffect" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_LuminosityAmount ("GrayScale Amount", Range(0.0, 1.0)) = 1.0
	}
	SubShader {
		Pass {
			CGPROGRAM
			#pragma vertex vert_img
			#pragma fragment frag

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			fixed _LuminosityAmount;

			fixed4 frag(v2f_img i) : COLOR
			{
				//Get the colors from the RenderTexture and the uv‘s
				//from the v2f_img struct
				fixed4 renderTex = tex2D(_MainTex, i.uv);

				//Apply the Luminosity values to our render texture
				float luminosity = 0.299 * renderTex.r + 0.587 * renderTex.g + 0.114 * renderTex.b;
				fixed4 finalColor = lerp(renderTex, luminosity, _LuminosityAmount);

				return finalColor;
			}

			ENDCG
		}
	}
	FallBack "Diffuse"
}

保存,返回Unity查看。把上述Shader赋给TestRenderImage.cs中的Shader变量。此时,我们改变grayScaleAmount的值就会发现游戏画面的灰度发生了变化(从左到右灰度值分别是0.0,0.5,1.0):

    

上面的过程展示了我们如何使用一种方便的方法检验画面特效Shader。下面,我们来更深入地了解这其中render texture到底发生了什么变化,以及从头到尾它们到底是怎么被处理的。

解释

画面特效是顺序处理的,这就像Photoshop中的layers。如果你有多个屏幕特效,你可以按顺序添加给该Camera,那么它们就会按照这个顺序被处理。

上述的过程是被简化过的,但通过这些我们可以看出画面特效的核心是如何工作的。

扩展内容

上面实现的是一个非常简单的画面特效系统。我们下面来看一下使用Unity的渲染器还可以做哪些有用的事情。

呜,这篇比较复杂,剩下的允许我明天来补坑。。。

时间: 2024-10-27 13:23:17

使用Unity Render Textures实现画面特效——建立画面特效脚本系统的相关文章

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

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

蛋哥的学习笔记之-基于Unity的Shader编程:X-1 音乐水波特效

蛋哥的学习笔记之-基于Unity的Shader编程:X-1 音乐水波特效 热度 13728 2015-7-11 23:34 |个人分类:蛋哥的学习笔记之-基于Unity的Shader编程| 音乐, Unity, Shader, 水波, Shader, Shader, Shader, Shader 一.要干些啥: 很久很久没有写文档了,前段时间做了个个人很喜欢的,自认为比较原创的小特效,所以写个文档纪念下(本人特别喜欢音乐) 思路其实很简单,首先用顶点着色器实现一般的水波特效,然后解析音频数据(我

WP8.1 UI 编程 六、变换特效和三维特效

1. 变换特效 变换原理:是二维变换矩阵 M11 M12 0 M21 M22 0 OffsetX OffsetY  1 WP只支持仿射变换,因此矩阵右边是0.0.1. (x,y,1)乘矩阵得到(x1,y1,1),新坐标为(x1,y1). 即:坐标(x,y)经矩阵变换后,新坐标为(x*M11 + y*M21 + OffsetX,x*M12 + y*M22 + OffsetY). WP提供了很多Transform类以变换对象,只需应用到UIElement的RenderTransform属性即可. 列

2.CCGridAction(3D效果),3D反转特效,凸透镜特效,液体特效,3D翻页特效,水波纹特效,3D晃动的特效,扭曲旋转特效,波动特效,3D波动特效

 1 类图组织 2 实例 CCSprite * spr = CCSprite::create("HelloWorld.png"); spr->setPosition(ccp(winSize.width/2,winSize.height/2)); addChild(spr); //GridAction //CCFlipX3D * action = CCFlipX3D::create(2); //CCFlipY3D * action = CCFlipY3D::create(2);

atitit.提升备份文件复制速度(3) ----建立同步删除脚本

atitit.提升备份文件复制速度(3) ----建立同步删除脚本 1. 建立同步删除脚本两个方法.. 1 2. 1从回收站info2文件... 1 3. 清理结束在后snap比较 1 4. Npp replace  gene del from lst 1 5. Code 2 1. 建立同步删除脚本两个方法.. 2. 1从回收站info2文件... Copy info2 {String s="G:\\RECYCLER\\S-1-5-21-602162358-1284227242-68200333

jQuery 特效网址(特效大全)

嘿嘿,虽然里面的特效都不是小码哥写的,不过,这个分享链接确实偶自己拿来分享给大家的,因此大家不要挑理儿说这个不是原创啦,嘎嘎 实时更新,最全特效之jQuery特效:http://www.jq22.com/

城管APP开发建立新的城管系统新模式

互联网+时代已经带来,为了顺应时代热潮,各大城市逐渐进行城管APP开发,城管APP软件的出现,不但增强了城市管理,强化城市公共管理领域,还可以提高城管工作效率,城管APP开发建立新的城管系统新模式. 城管APP开发需要做好以下几点 广州APP软件开发公司道屹道认为:城管APP开发的主要服务对象是政府城管部门以及人民群众,为双方搭建了一个交流沟通的平台,从而促进社会和谐发展.城管APP开发根据当前城市商贩摆摊以及社会发展情况,个性化的打造相对应的服务功能,让双方可以快速的处理城市问题,从而给广大人

Unity脚本系统

什么是脚本?脚本是一个 MonoBehavior, 继承关系是 MonoBehavior -> Behavior -> Component -> Object GameObject 的行为是由附加到他们身上的组件控制的. 游戏事件 MonoBehavior 类名和脚本名必须要一样 public class MainPlayer : MonoBehaviour { public string myName; // Use this for initialization void Start

DX笔记之六------游戏画面绘图之透明特效的制作方法

原文链接:http://blog.csdn.net/zhmxy555/article/details/7338082 透明效果 由于所有的图文件都是以矩形来储存的,我们也许会需要把一张怪兽图片贴到窗口的背景图上,而这种情况下如果直接进行贴图,结果如下图: 这似乎不是我们想要的结果. 为了得到透明效果,我们需要运用到BitBlt()贴图函数以及其参数Raster的值来将图片中不必要的部分去掉(又称去背),使得图中的主题可以与背景完美融合. 我们以图中的恐龙为例子,首先准备一张位图,如下图. 图中的