您的位置:首页 > 移动开发 > Unity3D

【浅墨Unity3D Shader编程】之八 Unity5新版Shader模板源码解析&运动模糊(径向模糊)屏幕特效的实现

2016-02-19 10:54 621 查看
本系列文章由@浅墨_毛星云 出品,转载请注明出处。

文章链接: http://blog.csdn.net/poem_qianmo/article/details/49405909
作者:毛星云(浅墨)
微博:http://weibo.com/u/1723155442

本文工程使用的Unity3D版本: 5.2.1

概要:本文对Unity5中全新的三种Shader模板的源码进行了解析,然后还讲解了运动模糊屏幕特效的实现方法。

前言

时隔9个月,终于有了一些稍微空闲的时间,可以进行一些更新了。

鉴于以后可以用来写博客的时间肯定不会非常充裕,个人觉得再讲Shader的基础写法比较拖节奏,所以最好是把力气用在刀刃上,在有时间更新的时候,主要讲一些更加前沿、实用的技巧,以及进阶一些的Shader的写法。

另外,从本次开始,每次更新的标题也就不取那么主题化了(比如之前的暗黑城堡、静谧之秋之类的)。人老了,渐渐文艺不起来了,还是实在一点好。:D

好的,就交代这么多。接下来就按新的风格试着写几次。。

看一组程序截图之后,便开始我们的正文。如下图,一份运动模糊屏幕特效的效果对比。

原始场景效果图:



开启运动模糊特效后的场景效果图:



图先就上这两张。文章末尾有更多的运行截图,并提供了源工程的下载。先放出可运行的exe下载,如下:

【可运行的exe游戏场景请点击这里下载试玩】

好的,正文开始。

一、Unity5中新的Shader体系简析

Unity5和之前的书写模式有了一定的改变。Unity5时代的Shader Reference官方文档也进一步地变得丰满。

主要需要了解到的是,在原来的Unity中,若想要新建一个Shader源文件,不考虑compute shader的话,仅有一种Shader模板供选择。而自从Unity5.1起(好像是Unity5.1)

想在Unity5.1之后的版本中新建Shader,【右键在Project窗口中单击】->【Create】,会出现如下的四个选项:



而由于暂时不考虑compute shader。所以,新版Unity中有三种基本的Shader模板分别为:

Standard Surface Shader标准表面着色器
Unlit Shader 无灯光着色器
Image Effect Shader 图像特效着色器

二、Unity5中新的Shader模板源码解析

下面,对Unity5中三种基本Shader模板进行逐行注释与思路解析。

可以点击这里跳转到Github,查看详细注释好的三种Shader模板的源码。

2.1 标准表面着色器(Standard
Surface Shader)模板源码解析

在Unity中,我们若要实现新的表面着色器时,可以根据这个模板,进行一步添加子着色器和新的参数与特性。

这个Shader模板的脉络很清晰,先是定义一些属性,然后在SubShader中设置渲染模式,层次细节LOD的值,然后开启一个CG编程语言模块,写一些编译指令#pragma,声明一下变量让属性值在CG块中可见,定义输入结构,然后填充一下表面着色函数即可。注意:专门强调一句,SurfaceShader不能使用Pass,一使用就报错,我们直接在SubShader中实现和填充代码就可以了。

Standard Surface Shader模板详细注释的Shader代码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/Surface Shader模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主颜色

_Color("Color", Color) = (1,1,1,1)

//主纹理

_MainTex("Albedo (RGB)", 2D) = "white" {}

//光泽度

_Glossiness("Smoothness", Range(0,1)) = 0.5

//金属度

_Metallic("Metallic", Range(0,1)) = 0.0

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//【注意:Surface Shader不能使用Pass,直接在SubShader中实现即可】

//渲染类型设置:不透明

Tags{"RenderType" = "Opaque" }

//细节层次设为:200

LOD200

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器表明着色函数的名称为surf

//Standard表示光照模型为Unity标准版光照模型

//fullforwardshadows表示在正向渲染路径中支持所有阴影类型

#pragma surface surf Standard fullforwardshadows

//编译指令: 指定着色器编译目标为Shader Model 3.0

#pragma target 3.0

//变量的声明

sampler2D _MainTex;

//表面输入结构体

struct Input

{

float2 uv_MainTex;//纹理坐标

};

//变量的声明

half _Glossiness;

half _Metallic;

fixed4 _Color;

//--------------------------------【表面着色函数】-----------------------------

//输入:表面输入结构体

//输出:Unity内置的SurfaceOutputStandard结构体

//SurfaceOutputStandard原型如下:

/*

struct SurfaceOutputStandard

{

fixed3 Albedo; // 漫反射颜色

fixed3 Normal; // 切线空间法线

half3 Emission; //自发光

half Metallic; // 金属度;取0为非金属, 取1为金属

half Smoothness; // 光泽度;取0为非常粗糙, 取1为非常光滑

half Occlusion; // 遮挡(默认值为1)

fixed Alpha; // 透明度

};

*/

//---------------------------------------------------------------------------------

void surf(Input IN, inout SurfaceOutputStandard o)

{

//【1】漫反射颜色为主纹理对应的纹理坐标,并乘以主颜色

fixed4c = tex2D(_MainTex, IN.uv_MainTex) * _Color;

//【2】将准备好的颜色的rgb分量作为漫反射颜色

o.Albedo= c.rgb;

//【3】金属度取自属性值

o.Metallic= _Metallic;

//【4】光泽度也取自属性值

o.Smoothness= _Glossiness;

//【5】将准备好的颜色的alpha分量作为Alpha分量值

o.Alpha= c.a;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

//备胎为漫反射

FallBack"Diffuse"

}

接着来看Unity5的第二种Shader模板,无灯光着色器(Unlit Shader)模板。

2.2 无灯光着色器(Unlit
Shader)模板源码解析

Unlit Shader,简单来说,就是直接采用漫反射纹理,不考虑场景中的任何灯光效果。使用无灯光着色器的话,也就不能使用任何镜面或者法线效果了。Unlit系的Shader基本原理和其他Shader无异,但是计算量更小,更快速,更高效。

而在Unity内置的各种着色器中,有如下的四种是Unlit系的:



好的,已经稍微解释了下什么是Unlit Shader。下面一起看一下Unity为我们提供的无灯光着色器模板的代码:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/无灯光着色器(Unlit Shader)模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主纹理

_MainTex("Texture", 2D) = "white" {}

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//渲染类型设置:不透明

Tags{ "RenderType"="Opaque" }

//细节层次设为:100

LOD 100

//--------------------------------唯一的通道-------------------------------

Pass

{

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//着色器变体快捷编译指令:雾效。编译出几个不同的Shader变体来处理不同类型的雾效(关闭/线性/指数/二阶指数)

#pragma multi_compile_fog

//包含头文件

#include"UnityCG.cginc"

//顶点着色器输入结构

struct appdata

{

float4 vertex : POSITION;//顶点位置

float2 uv : TEXCOORD0;//纹理坐标

};

//顶点着色器输出结构

struct v2f

{

float2 uv : TEXCOORD0;//纹理坐标

UNITY_FOG_COORDS(1)//雾数据

float4 vertex : SV_POSITION;//像素位置

};

//变量声明

sampler2D _MainTex;

float4 _MainTex_ST;

//--------------------------------【顶点着色函数】-----------------------------

//输入:顶点输入结构体

//输出:顶点输出结构体

//---------------------------------------------------------------------------------

v2f vert (appdata v)

{

//【1】实例化一个输入结构体

v2f o;

//【2】填充此输出结构

//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

o.vertex= mul(UNITY_MATRIX_MVP, v.vertex);

//【3】用UnityCG.cginc头文件中内置定义的宏,根据uv坐标来计算真正的纹理上对应的位置(按比例进行二维变换)

o.uv= TRANSFORM_TEX(v.uv, _MainTex);

//【4】用UnityCG.cginc头文件中内置定义的宏处理雾效,从顶点着色器中输出雾效数据

UNITY_TRANSFER_FOG(o,o.vertex);

//【5】返回此输出结构对象

return o;

}

//--------------------------------【片段着色函数】-----------------------------

//输入:顶点输出结构体

//输出:float4型的像素颜色值

//---------------------------------------------------------------------------------

fixed4 frag (v2f i) : SV_Target

{

//【1】采样主纹理在对应坐标下的颜色值

fixed4 col = tex2D(_MainTex, i.uv);

//【2】用UnityCG.cginc头文件中内置定义的宏启用雾效

UNITY_APPLY_FOG(i.fogCoord,col);

//【3】返回最终的颜色值

return col;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

不难分析得到,无灯光着色器是一种顶点&片段着色器,这边模板给出的是单子着色器,单通道的写法。

并且,无灯光着色器中使用了一些UnityCG.cginc头文件中内置的宏,比如说TRANSFORM_TEX、UNITY_TRANSFER_FOG、UNITY_APPLY_FOG。接下来分别把这三个宏简单解释一下。

2.2.1 TRANSFORM_TEX宏

TRANSFORM_TEX宏的定义为:

[cpp] view
plain copy

print?

#define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)

其位于UnityCG.cginc(Unity5.2.1版本)的第266行。其可以根据uv坐标来计算真正的纹理上对应的位置(按比例进行二维变换),组合上上文中定义的float4 _MainTex_ST,便可以计算真正的纹理上对应的位置。

2.2.2 UNITY_TRANSFER_FOG宏

UNITY_TRANSFER_FOG宏的作用是从顶点着色输出雾数据。在UnityCG.cginc(Unity5.2.1版本)的第772行起,具体定义如下:

[cpp] view
plain copy

print?

#if (SHADER_TARGET < 30) ||defined(SHADER_API_MOBILE)

//手机端或者Shader Mode 2.0: 计算每个顶点的雾效因子

#define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord =unityFogFactor

#else

//Shader Mode 3.0和PC和游戏机: 计算每像素的雾距离,和每像素的雾效因子

#define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z

#endif

2.2.3 UNITY_APPLY_FOG宏

UNITY_APPLY_FOG宏的定义稍微有些长,从UnityCG.cginc(Unity 5.2.1版本)的第787行起:

[cpp] view
plain copy

print?

#if defined(FOG_LINEAR) || defined(FOG_EXP)|| defined(FOG_EXP2)

#if(SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)

//mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp thecolor

#defineUNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,coord)

#else

//SM3.0 and PC/console: calculate fog factor and lerp fog color

#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR(coord);UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor)

#endif

#else

#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol)

#endif

#ifdef UNITY_PASS_FORWARDADD

#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0))

#else

#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)

#endif

可以发现,UNITY_APPLY_FOG宏的作用是从顶点着色器中输出雾效数据,将第二个参数中的颜色值作为雾效的颜色值,且在正向附加渲染通道(forward-additive pass)中,自动设置纯黑色(fixed4(0,0,0,0))的雾效。其在定义中借助了UNITY_APPLY_FOG_COLOR宏,而我们也可以使用UNITY_APPLY_FOG_COLOR来指定特定颜色的雾效。

2.3 图像特效着色器(Image
Effect Shader) 模板源码解析

这里的图像特效一般指的就是屏幕图像特效,在Camera加上各种滤镜,比如说屏幕溅血,像素化,色调的调整,画面模糊等效果。其也是一个顶点&片段着色器,且一般主要的操作集中在片段着色函数中。Unity为我们提供的模板,经过详细注释后的源码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/图像特效Shader模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主纹理

_MainTex("Texture", 2D) = "white" {}

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//关闭剔除操作

Cull Off

//关闭深度写入模式

ZWrite Off

//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTestOff)

ZTest Always

//--------------------------------唯一的通道-------------------------------

Pass

{

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//包含头文件

#include"UnityCG.cginc"

//顶点着色器输入结构

struct appdata

{

float4 vertex : POSITION;//顶点位置

float2 uv : TEXCOORD0;//一级纹理坐标

};

//顶点着色器输出结构(v2f,vertex to fragment)

struct v2f

{

float2 uv : TEXCOORD0;//一级纹理坐标

float4 vertex : SV_POSITION;//像素位置

};

//--------------------------------【顶点着色函数】-----------------------------

//输入:顶点输入结构体

//输出:顶点输出结构体

//---------------------------------------------------------------------------------

//顶点着色函数

v2f vert (appdata v)

{

//【1】实例化一个输入结构体

v2f o;

//【2】填充此输出结构

//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

o.vertex= mul(UNITY_MATRIX_MVP, v.vertex);

//输入的UV纹理坐标为顶点输出的坐标

o.uv= v.uv;

//【3】返回此输出结构对象

return o;

}

//变量的声明

sampler2D _MainTex;

//--------------------------------【片段着色函数】-----------------------------

//输入:顶点输出结构体

//输出:float4型的像素颜色值

//---------------------------------------------------------------------------------

fixed4 frag (v2f i) : SV_Target

{

//【1】采样主纹理在对应坐标下的颜色值

fixed4 col = tex2D(_MainTex, i.uv);

//【2】将颜色值反向

col= 1 - col;

//【3】返回最终的颜色值

return col;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

2.4 Shader模板中文注释格式调整版替换

其实可以将Unity5中自带的上述三个着色器模板,替换成上文中贴出来的、经过详细注释和格式调整的Shader模板,这样在每次新建Shader时,就已经得到了具有很高可读性的Shader模板了,非常便捷。

一定要吐槽的是,Unity5.2.1自带的三个Shader模板的缩进和空格完全是混用的,导致在通过他们新建出来的Shader里面写代码的时候,格式非常混乱,十分影响新版Unity中Shader的编码体验。很明显,准备此Shader模板的Unity开发人员的编码习惯有点欠缺,得在这里点名批评,轻喷一下。

浅墨在一发现他们格式有问题的时候就马上替换掉了,所以现在在Unity中写Shader代码的体验是非常棒的。这边也教大家如何替换掉自带的3个模板。

Unity中Shader模板的位置是…Unity\Editor\Data\Resources\ScriptTemplates,比如说Unity安装在D:\ProgramFiles\路径下,整体路径就是:

D:\ProgramFiles\Unity\Editor\Data\Resources\ScriptTemplates。

在此路径下的3个txt,即为对应的三个Shader模板文件:

83-Shader__Standard SurfaceShader-NewSurfaceShader.shader.txt
84-Shader__UnlitShader-NewUnlitShader.shader.txt
85-Shader__Image EffectShader-NewImageEffectShader.shader

这边已经将调整好格式,详细注释的三种模板准备好了,下载之后,找到上面提到的…Unity\Editor\Data\Resources\ScriptTemplates目录。替换掉对应的3个txt文件即可。需要注意的是,如果你想自己DIY Shader模板,需要将txt保存为UTF-8编码格式,否则可能会出现乱码。

替换的模板下载地址在这里:

【Unity5-Shader模板中文注释格式调整版替换文件】下载

另外还有一个小细节可以提一下。如果你安装了两个或者两个以上的Unity5.1之后版本的Unity,如果你替换你当前使用的Unity路径下的模板文件后,新建的模板文件没有改变的话,你试着将所有的Unity5.1之后版本的路径下的这三个模板文件都进行替换,应该就可以实现想要的替换效果。浅墨的机器上就是同时存在Unity5.2.1和Unity5.2.0,然后使用Unity5.2.1,替换掉Unity5.2.1路径下的三个模板文件后,并没有发生变换。之后我按图索骥,替换了Unity
5.2.0版路径下的三个模板文化,才使得替换的模板文件生效。这估计是Unity多版本共存时,自身的一个小bug。

三、运动模糊屏幕特效的实现

关于运动模糊特效,如果把握要要点的话,实现起来其实比较简单,就是一个脚本文件配合一个Shader,便可以实现较为出色的运动模糊特效。而其中的脚本文件用于控制Shader中的外部参数。

也就是说一个屏幕特效通常分为两部分来实现:

Shader实现部分
脚本实现部分

下面我们对运动模糊屏幕特效的实现分别进行简单的描述。

可以点击这里跳转到Github,查看详细注释好的运动模糊屏幕特效的实现源码。

3.1 Shader实现部分

先看一下Shader代码的写法,因为基本上已经逐行注释,就不花时间和笔墨仔细讲解了,详细注释的代码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/运动模糊特效标准版"

{

//------------------------------------【属性值】------------------------------------

Properties

{

_MainTex("主纹理 (RGB)", 2D) = "white" {}

_IterationNumber("迭代次数", Int)=16

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//--------------------------------唯一的通道-------------------------------

Pass

{

//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTest Off)

ZTest Always

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令: 指定着色器编译目标为Shader Model 3.0

#pragma target 3.0

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//包含辅助CG头文件

#include "UnityCG.cginc"

//外部变量的声明

uniform sampler2D _MainTex;

uniform float _Value;

uniform float _Value2;

uniform float _Value3;

uniform int _IterationNumber;

//顶点输入结构

struct vertexInput

{

float4 vertex : POSITION;//顶点位置

float4 color : COLOR;//颜色值

float2 texcoord : TEXCOORD0;//一级纹理坐标

};

//顶点输出结构

struct vertexOutput

{

half2 texcoord : TEXCOORD0;//一级纹理坐标

float4 vertex : SV_POSITION;//像素位置

fixed4 color : COLOR;//颜色值

};

//--------------------------------【顶点着色函数】-----------------------------

// 输入:顶点输入结构体

// 输出:顶点输出结构体

//---------------------------------------------------------------------------------

vertexOutput vert(vertexInput Input)

{

//【1】声明一个输出结构对象

vertexOutput Output;

//【2】填充此输出结构

//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

Output.vertex = mul(UNITY_MATRIX_MVP, Input.vertex);

//输出的纹理坐标也就是输入的纹理坐标

Output.texcoord = Input.texcoord;

//输出的颜色值也就是输入的颜色值

Output.color = Input.color;

//【3】返回此输出结构对象

return Output;

}

//--------------------------------【片段着色函数】-----------------------------

// 输入:顶点输出结构体

// 输出:float4型的颜色值

//---------------------------------------------------------------------------------

float4 frag(vertexOutput i) : COLOR

{

//【1】设置中心坐标

float2 center = float2(_Value2, _Value3);

//【2】获取纹理坐标的x,y坐标值

float2 uv = i.texcoord.xy;

//【3】纹理坐标按照中心位置进行一个偏移

uv -= center;

//【4】初始化一个颜色值

float4 color = float4(0.0, 0.0, 0.0, 0.0);

//【5】将Value乘以一个系数

_Value *= 0.085;

//【6】设置坐标缩放比例的值

float scale = 1;

//【7】进行纹理颜色的迭代

for (int j = 1; j < _IterationNumber; ++j)

{

//将主纹理在不同坐标采样下的颜色值进行迭代累加

color += tex2D(_MainTex, uv * scale + center);

//坐标缩放比例依据循环参数的改变而变化

scale = 1 + (float(j * _Value));

}

//【8】将最终的颜色值除以迭代次数,取平均值

color /= (float)_IterationNumber;

//【9】返回最终的颜色值

return color;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

可以发现,这是一个单子着色器、单通道的顶点&片段着色器,顶点着色函数vert中基本上都是写的比较中规中矩的代码,精髓之处在于片段着色器frag中,用一个for

循环,将像素颜色按照一条直线(uv * scale + center)进行了迭代采样累加,最终将采样的颜色的总和除以采样次数,得到了想要实现的运动模糊效果。

3.2
脚本实现部分

脚本文件的实现方面,如下的即个点是要提出来专门讲一下的,即Shader文件的获取方法和OnRenderImage函数、Blit函数。

3.2.1 Shader文件的获取

Shader文件的获取可以使用Shader.Find函数实现。需要注意,Shader.Find函数参数应该和Shader代码中的名称一致,也就是下面的代码框架中xxx的值,而不是Shader的文件名:

[cpp] view
plain copy

print?

Shader "xxxx"

{

}

举个例子,脚本代码如果是这样:

[cpp] view
plain copy

print?

CurShader = Shader.Find ("浅墨Shader编程/Volume8/运动模糊特效标准版");

那么获取到的Shader,文件名是任意的,但Shader代码框架肯定是这样:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/运动模糊特效标准版"

{

……

}

3.2.2
OnRenderImage函数与Blit函数

OnRenderImage()函数是MonoBehaviour中提供的一个可供我们重写的函数,它在unity完成所有图片的渲染后被调用。所以我们想实现屏幕特效,主要依靠它来实现。而OnRenderImage函数的函数原型是:

[cpp] view
plain copy

print?

void OnRenderImage(RenderTexture sourceTexture,RenderTexture destTexture);

另外,我们需要配合一个Graphics.Blit函数,实现从源纹理到目标渲染纹理的拷贝过程,其原型如下三种:

[cpp] view
plain copy

print?

public static void Blit(Texture source,RenderTexture dest);

public static void Blit(Texture source,RenderTexture dest, Material mat, int pass = -1);

public static void Blit(Texture source,Material mat, int pass = -1);

其中。

第一个参数,Texture类型的source,原始纹理。

第二个参数,RenderTexture类型的dest,目标渲染纹理,若为null,表示直接将原始纹理拷贝到屏幕之上。

第三个参数,Material类型的mat,使用的材质(其实也就是Shader),根据不同材质的准备,就是在这里实现后期的效果的。

第四个参数,int类型的pass,有默认值 -1,表示使用所有的pass。用于指定使用哪一个pass。

说个题外话,其实在很久之前的Win32 API游戏编程中,同样原理和相似用途的Blit函数用得太多了。

好的,最后看一下实现屏幕特效的核心代码,如下:

[cpp] view
plain copy

print?

void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)

{

//着色器实例不为空,就进行参数设置

if (CurShader != null)

{

//设置Shader中的外部变量

material.SetFloat("_IterationNumber", IterationNumber);

material.SetFloat("_Value", Intensity);

material.SetFloat("_Value2", OffsetX);

material.SetFloat("_Value3", OffsetY);

material.SetFloat("_Value4", blurWidth);

material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));

//拷贝源纹理到目标渲染纹理,加上我们的材质效果

Graphics.Blit(sourceTexture, destTexture, material);

}

//着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的

else

{

//直接拷贝源纹理到目标渲染纹理

Graphics.Blit(sourceTexture, destTexture);

}

最后看一下详细注释后的脚本完整实现代码:

[csharp] view
plain copy

print?

using UnityEngine;

using System.Collections;

[ExecuteInEditMode]

public class MotionBlurEffects : MonoBehaviour

{

//-------------------变量声明部分-------------------

#region Variables

public Shader CurShader;//着色器实例

private Vector4 ScreenResolution;//屏幕分辨率

private Material CurMaterial;//当前的材质

[Range(5, 50)]

public float IterationNumber = 15;

[Range(-0.5f, 0.5f)]

public float Intensity = 0.125f;

[Range(-2f, 2f)]

public float OffsetX = 0.5f;

[Range(-2f, 2f)]

public float OffsetY = 0.5f;

public static float ChangeValue;

public static float ChangeValue2;

public static float ChangeValue3;

public static float ChangeValue4;

#endregion

//-------------------------材质的get&set----------------------------

#region MaterialGetAndSet

Material material

{

get

{

if (CurMaterial == null)

{

CurMaterial = new Material(CurShader);

CurMaterial.hideFlags = HideFlags.HideAndDontSave;

}

return CurMaterial;

}

}

#endregion

//-----------------------------------------【Start()函数】---------------------------------------------

// 说明:此函数仅在Update函数第一次被调用前被调用

//--------------------------------------------------------------------------------------------------------

void Start()

{

//依此赋值

ChangeValue = Intensity;

ChangeValue2 = OffsetX;

ChangeValue3 = OffsetY;

ChangeValue4 = IterationNumber;

//找到当前的Shader文件

CurShader = Shader.Find("浅墨Shader编程/Volume8/运动模糊特效标准版");

//判断是否支持屏幕特效

if (!SystemInfo.supportsImageEffects)

{

enabled = false;

return;

}

}

//-------------------------------------【OnRenderImage()函数】------------------------------------

// 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果

//--------------------------------------------------------------------------------------------------------

void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)

{

//着色器实例不为空,就进行参数设置

if (CurShader != null)

{

//设置Shader中的外部变量

material.SetFloat("_IterationNumber", IterationNumber);

material.SetFloat("_Value", Intensity);

material.SetFloat("_Value2", OffsetX);

material.SetFloat("_Value3", OffsetY);

material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));

//拷贝源纹理到目标渲染纹理,加上我们的材质效果

Graphics.Blit(sourceTexture, destTexture, material);

}

//着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的

else

{

//直接拷贝源纹理到目标渲染纹理

Graphics.Blit(sourceTexture, destTexture);

}

}

//-----------------------------------------【OnValidate()函数】--------------------------------------

// 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用

//--------------------------------------------------------------------------------------------------------

void OnValidate()

{

//将编辑器中的值赋值回来,确保在编辑器中值的改变立刻让结果生效

ChangeValue4 = IterationNumber;

ChangeValue = Intensity;

ChangeValue2 = OffsetX;

ChangeValue3 = OffsetY;

}

//-----------------------------------------【Update()函数】------------------------------------------

// 说明:此函数在每一帧中都会被调用

//--------------------------------------------------------------------------------------------------------

void Update()

{

if (Application.isPlaying)

{

//赋值

IterationNumber = ChangeValue4;

Intensity = ChangeValue;

OffsetX = ChangeValue2;

OffsetY = ChangeValue3;

}

//找到对应的Shader文件

#if UNITY_EDITOR

if (Application.isPlaying != true)

{

CurShader = Shader.Find("浅墨Shader编程/Volume8/运动模糊特效标准版");

}

#endif

}

//-----------------------------------------【OnDisable()函数】---------------------------------------

// 说明:当对象变为不可用或非激活状态时此函数便被调用

//--------------------------------------------------------------------------------------------------------

void OnDisable()

{

if (CurMaterial)

{

DestroyImmediate(CurMaterial);

}

}

}


3.3 关于如何使用此特效

使用方面的话比较简单,把脚本文件拖到主摄像机上面,效果就出来了。

脚本文件中有如下这些参数可以调整,得到不同的模糊效果:



Iteration Number- 迭代次数
Intensity - 模糊强度
Offset X - X方向上的偏移
Offset Y - Y方向上的偏移

四、最终的效果展示

这边贴几张场景的效果图和使用了屏幕特效后的效果图。需要注意的是,本次的场景效果,除了类似CS/CF的FPS游戏的控制系统以外,还可以使用键盘上的按键【F】,开启或者关闭运动模糊特效。正如下图所展示的:



下面放几张测试截图。

首先,Unity5中,导出的exe使用了新的片头Logo,质感不错,好评:



城镇中的原始效果:



运动模糊后的效果:



海港原始效果:



海港运动模糊后的效果:



城镇草丛前的效果:



城镇草丛前运动模糊后的效果:



沙滩原始效果:



沙滩运动模糊后的效果:



海面原始效果:



海面运动模糊后的效果:



五、后记

本来准备这次更新再稍微剖析一下Unity5中主推的Standard Shader的写法思路的,但发现这篇博文的篇幅已经有点长了,那么,StandardShader就留到下次更新再讲。

本次的更新大致如此,以后的更新依然是安排在每周一,最近一段时间尽量保证每周都更。

最后,感谢各位捧场,我们下周再见。

附: 本博文相关下载链接清单

【百度云】博文示例场景exe试玩下载

【百度云】博文示例场景所有资源与源码的Unitypackage合集下载

【百度云】【Unity5-Shader模板中文注释格式调整版替换文件】下载

【Github】Unity5新版模板源码逐行注释

【Github】运动模糊效果实现源码

本系列文章由@浅墨_毛星云 出品,转载请注明出处。

文章链接: http://blog.csdn.net/poem_qianmo/article/details/49405909
作者:毛星云(浅墨)
微博:http://weibo.com/u/1723155442

本文工程使用的Unity3D版本: 5.2.1

概要:本文对Unity5中全新的三种Shader模板的源码进行了解析,然后还讲解了运动模糊屏幕特效的实现方法。

前言

时隔9个月,终于有了一些稍微空闲的时间,可以进行一些更新了。

鉴于以后可以用来写博客的时间肯定不会非常充裕,个人觉得再讲Shader的基础写法比较拖节奏,所以最好是把力气用在刀刃上,在有时间更新的时候,主要讲一些更加前沿、实用的技巧,以及进阶一些的Shader的写法。

另外,从本次开始,每次更新的标题也就不取那么主题化了(比如之前的暗黑城堡、静谧之秋之类的)。人老了,渐渐文艺不起来了,还是实在一点好。:D

好的,就交代这么多。接下来就按新的风格试着写几次。。

看一组程序截图之后,便开始我们的正文。如下图,一份运动模糊屏幕特效的效果对比。

原始场景效果图:



开启运动模糊特效后的场景效果图:



图先就上这两张。文章末尾有更多的运行截图,并提供了源工程的下载。先放出可运行的exe下载,如下:

【可运行的exe游戏场景请点击这里下载试玩】

好的,正文开始。

一、Unity5中新的Shader体系简析

Unity5和之前的书写模式有了一定的改变。Unity5时代的Shader Reference官方文档也进一步地变得丰满。

主要需要了解到的是,在原来的Unity中,若想要新建一个Shader源文件,不考虑compute shader的话,仅有一种Shader模板供选择。而自从Unity5.1起(好像是Unity5.1)

想在Unity5.1之后的版本中新建Shader,【右键在Project窗口中单击】->【Create】,会出现如下的四个选项:



而由于暂时不考虑compute shader。所以,新版Unity中有三种基本的Shader模板分别为:

Standard Surface Shader标准表面着色器
Unlit Shader 无灯光着色器
Image Effect Shader 图像特效着色器

二、Unity5中新的Shader模板源码解析

下面,对Unity5中三种基本Shader模板进行逐行注释与思路解析。

可以点击这里跳转到Github,查看详细注释好的三种Shader模板的源码。

2.1 标准表面着色器(Standard
Surface Shader)模板源码解析

在Unity中,我们若要实现新的表面着色器时,可以根据这个模板,进行一步添加子着色器和新的参数与特性。

这个Shader模板的脉络很清晰,先是定义一些属性,然后在SubShader中设置渲染模式,层次细节LOD的值,然后开启一个CG编程语言模块,写一些编译指令#pragma,声明一下变量让属性值在CG块中可见,定义输入结构,然后填充一下表面着色函数即可。注意:专门强调一句,SurfaceShader不能使用Pass,一使用就报错,我们直接在SubShader中实现和填充代码就可以了。

Standard Surface Shader模板详细注释的Shader代码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/Surface Shader模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主颜色

_Color("Color", Color) = (1,1,1,1)

//主纹理

_MainTex("Albedo (RGB)", 2D) = "white" {}

//光泽度

_Glossiness("Smoothness", Range(0,1)) = 0.5

//金属度

_Metallic("Metallic", Range(0,1)) = 0.0

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//【注意:Surface Shader不能使用Pass,直接在SubShader中实现即可】

//渲染类型设置:不透明

Tags{"RenderType" = "Opaque" }

//细节层次设为:200

LOD200

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器表明着色函数的名称为surf

//Standard表示光照模型为Unity标准版光照模型

//fullforwardshadows表示在正向渲染路径中支持所有阴影类型

#pragma surface surf Standard fullforwardshadows

//编译指令: 指定着色器编译目标为Shader Model 3.0

#pragma target 3.0

//变量的声明

sampler2D _MainTex;

//表面输入结构体

struct Input

{

float2 uv_MainTex;//纹理坐标

};

//变量的声明

half _Glossiness;

half _Metallic;

fixed4 _Color;

//--------------------------------【表面着色函数】-----------------------------

//输入:表面输入结构体

//输出:Unity内置的SurfaceOutputStandard结构体

//SurfaceOutputStandard原型如下:

/*

struct SurfaceOutputStandard

{

fixed3 Albedo; // 漫反射颜色

fixed3 Normal; // 切线空间法线

half3 Emission; //自发光

half Metallic; // 金属度;取0为非金属, 取1为金属

half Smoothness; // 光泽度;取0为非常粗糙, 取1为非常光滑

half Occlusion; // 遮挡(默认值为1)

fixed Alpha; // 透明度

};

*/

//---------------------------------------------------------------------------------

void surf(Input IN, inout SurfaceOutputStandard o)

{

//【1】漫反射颜色为主纹理对应的纹理坐标,并乘以主颜色

fixed4c = tex2D(_MainTex, IN.uv_MainTex) * _Color;

//【2】将准备好的颜色的rgb分量作为漫反射颜色

o.Albedo= c.rgb;

//【3】金属度取自属性值

o.Metallic= _Metallic;

//【4】光泽度也取自属性值

o.Smoothness= _Glossiness;

//【5】将准备好的颜色的alpha分量作为Alpha分量值

o.Alpha= c.a;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

//备胎为漫反射

FallBack"Diffuse"

}

接着来看Unity5的第二种Shader模板,无灯光着色器(Unlit Shader)模板。

2.2 无灯光着色器(Unlit
Shader)模板源码解析

Unlit Shader,简单来说,就是直接采用漫反射纹理,不考虑场景中的任何灯光效果。使用无灯光着色器的话,也就不能使用任何镜面或者法线效果了。Unlit系的Shader基本原理和其他Shader无异,但是计算量更小,更快速,更高效。

而在Unity内置的各种着色器中,有如下的四种是Unlit系的:



好的,已经稍微解释了下什么是Unlit Shader。下面一起看一下Unity为我们提供的无灯光着色器模板的代码:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/无灯光着色器(Unlit Shader)模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主纹理

_MainTex("Texture", 2D) = "white" {}

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//渲染类型设置:不透明

Tags{ "RenderType"="Opaque" }

//细节层次设为:100

LOD 100

//--------------------------------唯一的通道-------------------------------

Pass

{

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//着色器变体快捷编译指令:雾效。编译出几个不同的Shader变体来处理不同类型的雾效(关闭/线性/指数/二阶指数)

#pragma multi_compile_fog

//包含头文件

#include"UnityCG.cginc"

//顶点着色器输入结构

struct appdata

{

float4 vertex : POSITION;//顶点位置

float2 uv : TEXCOORD0;//纹理坐标

};

//顶点着色器输出结构

struct v2f

{

float2 uv : TEXCOORD0;//纹理坐标

UNITY_FOG_COORDS(1)//雾数据

float4 vertex : SV_POSITION;//像素位置

};

//变量声明

sampler2D _MainTex;

float4 _MainTex_ST;

//--------------------------------【顶点着色函数】-----------------------------

//输入:顶点输入结构体

//输出:顶点输出结构体

//---------------------------------------------------------------------------------

v2f vert (appdata v)

{

//【1】实例化一个输入结构体

v2f o;

//【2】填充此输出结构

//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

o.vertex= mul(UNITY_MATRIX_MVP, v.vertex);

//【3】用UnityCG.cginc头文件中内置定义的宏,根据uv坐标来计算真正的纹理上对应的位置(按比例进行二维变换)

o.uv= TRANSFORM_TEX(v.uv, _MainTex);

//【4】用UnityCG.cginc头文件中内置定义的宏处理雾效,从顶点着色器中输出雾效数据

UNITY_TRANSFER_FOG(o,o.vertex);

//【5】返回此输出结构对象

return o;

}

//--------------------------------【片段着色函数】-----------------------------

//输入:顶点输出结构体

//输出:float4型的像素颜色值

//---------------------------------------------------------------------------------

fixed4 frag (v2f i) : SV_Target

{

//【1】采样主纹理在对应坐标下的颜色值

fixed4 col = tex2D(_MainTex, i.uv);

//【2】用UnityCG.cginc头文件中内置定义的宏启用雾效

UNITY_APPLY_FOG(i.fogCoord,col);

//【3】返回最终的颜色值

return col;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

不难分析得到,无灯光着色器是一种顶点&片段着色器,这边模板给出的是单子着色器,单通道的写法。

并且,无灯光着色器中使用了一些UnityCG.cginc头文件中内置的宏,比如说TRANSFORM_TEX、UNITY_TRANSFER_FOG、UNITY_APPLY_FOG。接下来分别把这三个宏简单解释一下。

2.2.1 TRANSFORM_TEX宏

TRANSFORM_TEX宏的定义为:

[cpp] view
plain copy

print?

#define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)

其位于UnityCG.cginc(Unity5.2.1版本)的第266行。其可以根据uv坐标来计算真正的纹理上对应的位置(按比例进行二维变换),组合上上文中定义的float4 _MainTex_ST,便可以计算真正的纹理上对应的位置。

2.2.2 UNITY_TRANSFER_FOG宏

UNITY_TRANSFER_FOG宏的作用是从顶点着色输出雾数据。在UnityCG.cginc(Unity5.2.1版本)的第772行起,具体定义如下:

[cpp] view
plain copy

print?

#if (SHADER_TARGET < 30) ||defined(SHADER_API_MOBILE)

//手机端或者Shader Mode 2.0: 计算每个顶点的雾效因子

#define UNITY_TRANSFER_FOG(o,outpos) UNITY_CALC_FOG_FACTOR((outpos).z); o.fogCoord =unityFogFactor

#else

//Shader Mode 3.0和PC和游戏机: 计算每像素的雾距离,和每像素的雾效因子

#define UNITY_TRANSFER_FOG(o,outpos) o.fogCoord = (outpos).z

#endif

2.2.3 UNITY_APPLY_FOG宏

UNITY_APPLY_FOG宏的定义稍微有些长,从UnityCG.cginc(Unity 5.2.1版本)的第787行起:

[cpp] view
plain copy

print?

#if defined(FOG_LINEAR) || defined(FOG_EXP)|| defined(FOG_EXP2)

#if(SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)

//mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp thecolor

#defineUNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,coord)

#else

//SM3.0 and PC/console: calculate fog factor and lerp fog color

#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR(coord);UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor)

#endif

#else

#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol)

#endif

#ifdef UNITY_PASS_FORWARDADD

#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0))

#else

#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)

#endif

可以发现,UNITY_APPLY_FOG宏的作用是从顶点着色器中输出雾效数据,将第二个参数中的颜色值作为雾效的颜色值,且在正向附加渲染通道(forward-additive pass)中,自动设置纯黑色(fixed4(0,0,0,0))的雾效。其在定义中借助了UNITY_APPLY_FOG_COLOR宏,而我们也可以使用UNITY_APPLY_FOG_COLOR来指定特定颜色的雾效。

2.3 图像特效着色器(Image
Effect Shader) 模板源码解析

这里的图像特效一般指的就是屏幕图像特效,在Camera加上各种滤镜,比如说屏幕溅血,像素化,色调的调整,画面模糊等效果。其也是一个顶点&片段着色器,且一般主要的操作集中在片段着色函数中。Unity为我们提供的模板,经过详细注释后的源码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/图像特效Shader模板"

{

//------------------------------------【属性值】------------------------------------

Properties

{

//主纹理

_MainTex("Texture", 2D) = "white" {}

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//关闭剔除操作

Cull Off

//关闭深度写入模式

ZWrite Off

//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTestOff)

ZTest Always

//--------------------------------唯一的通道-------------------------------

Pass

{

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//包含头文件

#include"UnityCG.cginc"

//顶点着色器输入结构

struct appdata

{

float4 vertex : POSITION;//顶点位置

float2 uv : TEXCOORD0;//一级纹理坐标

};

//顶点着色器输出结构(v2f,vertex to fragment)

struct v2f

{

float2 uv : TEXCOORD0;//一级纹理坐标

float4 vertex : SV_POSITION;//像素位置

};

//--------------------------------【顶点着色函数】-----------------------------

//输入:顶点输入结构体

//输出:顶点输出结构体

//---------------------------------------------------------------------------------

//顶点着色函数

v2f vert (appdata v)

{

//【1】实例化一个输入结构体

v2f o;

//【2】填充此输出结构

//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

o.vertex= mul(UNITY_MATRIX_MVP, v.vertex);

//输入的UV纹理坐标为顶点输出的坐标

o.uv= v.uv;

//【3】返回此输出结构对象

return o;

}

//变量的声明

sampler2D _MainTex;

//--------------------------------【片段着色函数】-----------------------------

//输入:顶点输出结构体

//输出:float4型的像素颜色值

//---------------------------------------------------------------------------------

fixed4 frag (v2f i) : SV_Target

{

//【1】采样主纹理在对应坐标下的颜色值

fixed4 col = tex2D(_MainTex, i.uv);

//【2】将颜色值反向

col= 1 - col;

//【3】返回最终的颜色值

return col;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

2.4 Shader模板中文注释格式调整版替换

其实可以将Unity5中自带的上述三个着色器模板,替换成上文中贴出来的、经过详细注释和格式调整的Shader模板,这样在每次新建Shader时,就已经得到了具有很高可读性的Shader模板了,非常便捷。

一定要吐槽的是,Unity5.2.1自带的三个Shader模板的缩进和空格完全是混用的,导致在通过他们新建出来的Shader里面写代码的时候,格式非常混乱,十分影响新版Unity中Shader的编码体验。很明显,准备此Shader模板的Unity开发人员的编码习惯有点欠缺,得在这里点名批评,轻喷一下。

浅墨在一发现他们格式有问题的时候就马上替换掉了,所以现在在Unity中写Shader代码的体验是非常棒的。这边也教大家如何替换掉自带的3个模板。

Unity中Shader模板的位置是…Unity\Editor\Data\Resources\ScriptTemplates,比如说Unity安装在D:\ProgramFiles\路径下,整体路径就是:

D:\ProgramFiles\Unity\Editor\Data\Resources\ScriptTemplates。

在此路径下的3个txt,即为对应的三个Shader模板文件:

83-Shader__Standard SurfaceShader-NewSurfaceShader.shader.txt
84-Shader__UnlitShader-NewUnlitShader.shader.txt
85-Shader__Image EffectShader-NewImageEffectShader.shader

这边已经将调整好格式,详细注释的三种模板准备好了,下载之后,找到上面提到的…Unity\Editor\Data\Resources\ScriptTemplates目录。替换掉对应的3个txt文件即可。需要注意的是,如果你想自己DIY Shader模板,需要将txt保存为UTF-8编码格式,否则可能会出现乱码。

替换的模板下载地址在这里:

【Unity5-Shader模板中文注释格式调整版替换文件】下载

另外还有一个小细节可以提一下。如果你安装了两个或者两个以上的Unity5.1之后版本的Unity,如果你替换你当前使用的Unity路径下的模板文件后,新建的模板文件没有改变的话,你试着将所有的Unity5.1之后版本的路径下的这三个模板文件都进行替换,应该就可以实现想要的替换效果。浅墨的机器上就是同时存在Unity5.2.1和Unity5.2.0,然后使用Unity5.2.1,替换掉Unity5.2.1路径下的三个模板文件后,并没有发生变换。之后我按图索骥,替换了Unity
5.2.0版路径下的三个模板文化,才使得替换的模板文件生效。这估计是Unity多版本共存时,自身的一个小bug。

三、运动模糊屏幕特效的实现

关于运动模糊特效,如果把握要要点的话,实现起来其实比较简单,就是一个脚本文件配合一个Shader,便可以实现较为出色的运动模糊特效。而其中的脚本文件用于控制Shader中的外部参数。

也就是说一个屏幕特效通常分为两部分来实现:

Shader实现部分
脚本实现部分

下面我们对运动模糊屏幕特效的实现分别进行简单的描述。

可以点击这里跳转到Github,查看详细注释好的运动模糊屏幕特效的实现源码。

3.1 Shader实现部分

先看一下Shader代码的写法,因为基本上已经逐行注释,就不花时间和笔墨仔细讲解了,详细注释的代码如下:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/运动模糊特效标准版"

{

//------------------------------------【属性值】------------------------------------

Properties

{

_MainTex("主纹理 (RGB)", 2D) = "white" {}

_IterationNumber("迭代次数", Int)=16

}

//------------------------------------【唯一的子着色器】------------------------------------

SubShader

{

//--------------------------------唯一的通道-------------------------------

Pass

{

//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTest Off)

ZTest Always

//===========开启CG着色器语言编写模块===========

CGPROGRAM

//编译指令: 指定着色器编译目标为Shader Model 3.0

#pragma target 3.0

//编译指令:告知编译器顶点和片段着色函数的名称

#pragma vertex vert

#pragma fragment frag

//包含辅助CG头文件

#include "UnityCG.cginc"

//外部变量的声明

uniform sampler2D _MainTex;

uniform float _Value;

uniform float _Value2;

uniform float _Value3;

uniform int _IterationNumber;

//顶点输入结构

struct vertexInput

{

float4 vertex : POSITION;//顶点位置

float4 color : COLOR;//颜色值

float2 texcoord : TEXCOORD0;//一级纹理坐标

};

//顶点输出结构

struct vertexOutput

{

half2 texcoord : TEXCOORD0;//一级纹理坐标

float4 vertex : SV_POSITION;//像素位置

fixed4 color : COLOR;//颜色值

};

//--------------------------------【顶点着色函数】-----------------------------

// 输入:顶点输入结构体

// 输出:顶点输出结构体

//---------------------------------------------------------------------------------

vertexOutput vert(vertexInput Input)

{

//【1】声明一个输出结构对象

vertexOutput Output;

//【2】填充此输出结构

//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口

Output.vertex = mul(UNITY_MATRIX_MVP, Input.vertex);

//输出的纹理坐标也就是输入的纹理坐标

Output.texcoord = Input.texcoord;

//输出的颜色值也就是输入的颜色值

Output.color = Input.color;

//【3】返回此输出结构对象

return Output;

}

//--------------------------------【片段着色函数】-----------------------------

// 输入:顶点输出结构体

// 输出:float4型的颜色值

//---------------------------------------------------------------------------------

float4 frag(vertexOutput i) : COLOR

{

//【1】设置中心坐标

float2 center = float2(_Value2, _Value3);

//【2】获取纹理坐标的x,y坐标值

float2 uv = i.texcoord.xy;

//【3】纹理坐标按照中心位置进行一个偏移

uv -= center;

//【4】初始化一个颜色值

float4 color = float4(0.0, 0.0, 0.0, 0.0);

//【5】将Value乘以一个系数

_Value *= 0.085;

//【6】设置坐标缩放比例的值

float scale = 1;

//【7】进行纹理颜色的迭代

for (int j = 1; j < _IterationNumber; ++j)

{

//将主纹理在不同坐标采样下的颜色值进行迭代累加

color += tex2D(_MainTex, uv * scale + center);

//坐标缩放比例依据循环参数的改变而变化

scale = 1 + (float(j * _Value));

}

//【8】将最终的颜色值除以迭代次数,取平均值

color /= (float)_IterationNumber;

//【9】返回最终的颜色值

return color;

}

//===========结束CG着色器语言编写模块===========

ENDCG

}

}

}

可以发现,这是一个单子着色器、单通道的顶点&片段着色器,顶点着色函数vert中基本上都是写的比较中规中矩的代码,精髓之处在于片段着色器frag中,用一个for

循环,将像素颜色按照一条直线(uv * scale + center)进行了迭代采样累加,最终将采样的颜色的总和除以采样次数,得到了想要实现的运动模糊效果。

3.2
脚本实现部分

脚本文件的实现方面,如下的即个点是要提出来专门讲一下的,即Shader文件的获取方法和OnRenderImage函数、Blit函数。

3.2.1 Shader文件的获取

Shader文件的获取可以使用Shader.Find函数实现。需要注意,Shader.Find函数参数应该和Shader代码中的名称一致,也就是下面的代码框架中xxx的值,而不是Shader的文件名:

[cpp] view
plain copy

print?

Shader "xxxx"

{

}

举个例子,脚本代码如果是这样:

[cpp] view
plain copy

print?

CurShader = Shader.Find ("浅墨Shader编程/Volume8/运动模糊特效标准版");

那么获取到的Shader,文件名是任意的,但Shader代码框架肯定是这样:

[cpp] view
plain copy

print?

Shader "浅墨Shader编程/Volume8/运动模糊特效标准版"

{

……

}

3.2.2
OnRenderImage函数与Blit函数

OnRenderImage()函数是MonoBehaviour中提供的一个可供我们重写的函数,它在unity完成所有图片的渲染后被调用。所以我们想实现屏幕特效,主要依靠它来实现。而OnRenderImage函数的函数原型是:

[cpp] view
plain copy

print?

void OnRenderImage(RenderTexture sourceTexture,RenderTexture destTexture);

另外,我们需要配合一个Graphics.Blit函数,实现从源纹理到目标渲染纹理的拷贝过程,其原型如下三种:

[cpp] view
plain copy

print?

public static void Blit(Texture source,RenderTexture dest);

public static void Blit(Texture source,RenderTexture dest, Material mat, int pass = -1);

public static void Blit(Texture source,Material mat, int pass = -1);

其中。

第一个参数,Texture类型的source,原始纹理。

第二个参数,RenderTexture类型的dest,目标渲染纹理,若为null,表示直接将原始纹理拷贝到屏幕之上。

第三个参数,Material类型的mat,使用的材质(其实也就是Shader),根据不同材质的准备,就是在这里实现后期的效果的。

第四个参数,int类型的pass,有默认值 -1,表示使用所有的pass。用于指定使用哪一个pass。

说个题外话,其实在很久之前的Win32 API游戏编程中,同样原理和相似用途的Blit函数用得太多了。

好的,最后看一下实现屏幕特效的核心代码,如下:

[cpp] view
plain copy

print?

void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)

{

//着色器实例不为空,就进行参数设置

if (CurShader != null)

{

//设置Shader中的外部变量

material.SetFloat("_IterationNumber", IterationNumber);

material.SetFloat("_Value", Intensity);

material.SetFloat("_Value2", OffsetX);

material.SetFloat("_Value3", OffsetY);

material.SetFloat("_Value4", blurWidth);

material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));

//拷贝源纹理到目标渲染纹理,加上我们的材质效果

Graphics.Blit(sourceTexture, destTexture, material);

}

//着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的

else

{

//直接拷贝源纹理到目标渲染纹理

Graphics.Blit(sourceTexture, destTexture);

}

最后看一下详细注释后的脚本完整实现代码:

[csharp] view
plain copy

print?

using UnityEngine;

using System.Collections;

[ExecuteInEditMode]

public class MotionBlurEffects : MonoBehaviour

{

//-------------------变量声明部分-------------------

#region Variables

public Shader CurShader;//着色器实例

private Vector4 ScreenResolution;//屏幕分辨率

private Material CurMaterial;//当前的材质

[Range(5, 50)]

public float IterationNumber = 15;

[Range(-0.5f, 0.5f)]

public float Intensity = 0.125f;

[Range(-2f, 2f)]

public float OffsetX = 0.5f;

[Range(-2f, 2f)]

public float OffsetY = 0.5f;

public static float ChangeValue;

public static float ChangeValue2;

public static float ChangeValue3;

public static float ChangeValue4;

#endregion

//-------------------------材质的get&set----------------------------

#region MaterialGetAndSet

Material material

{

get

{

if (CurMaterial == null)

{

CurMaterial = new Material(CurShader);

CurMaterial.hideFlags = HideFlags.HideAndDontSave;

}

return CurMaterial;

}

}

#endregion

//-----------------------------------------【Start()函数】---------------------------------------------

// 说明:此函数仅在Update函数第一次被调用前被调用

//--------------------------------------------------------------------------------------------------------

void Start()

{

//依此赋值

ChangeValue = Intensity;

ChangeValue2 = OffsetX;

ChangeValue3 = OffsetY;

ChangeValue4 = IterationNumber;

//找到当前的Shader文件

CurShader = Shader.Find("浅墨Shader编程/Volume8/运动模糊特效标准版");

//判断是否支持屏幕特效

if (!SystemInfo.supportsImageEffects)

{

enabled = false;

return;

}

}

//-------------------------------------【OnRenderImage()函数】------------------------------------

// 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果

//--------------------------------------------------------------------------------------------------------

void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)

{

//着色器实例不为空,就进行参数设置

if (CurShader != null)

{

//设置Shader中的外部变量

material.SetFloat("_IterationNumber", IterationNumber);

material.SetFloat("_Value", Intensity);

material.SetFloat("_Value2", OffsetX);

material.SetFloat("_Value3", OffsetY);

material.SetVector("_ScreenResolution", new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f));

//拷贝源纹理到目标渲染纹理,加上我们的材质效果

Graphics.Blit(sourceTexture, destTexture, material);

}

//着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的

else

{

//直接拷贝源纹理到目标渲染纹理

Graphics.Blit(sourceTexture, destTexture);

}

}

//-----------------------------------------【OnValidate()函数】--------------------------------------

// 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用

//--------------------------------------------------------------------------------------------------------

void OnValidate()

{

//将编辑器中的值赋值回来,确保在编辑器中值的改变立刻让结果生效

ChangeValue4 = IterationNumber;

ChangeValue = Intensity;

ChangeValue2 = OffsetX;

ChangeValue3 = OffsetY;

}

//-----------------------------------------【Update()函数】------------------------------------------

// 说明:此函数在每一帧中都会被调用

//--------------------------------------------------------------------------------------------------------

void Update()

{

if (Application.isPlaying)

{

//赋值

IterationNumber = ChangeValue4;

Intensity = ChangeValue;

OffsetX = ChangeValue2;

OffsetY = ChangeValue3;

}

//找到对应的Shader文件

#if UNITY_EDITOR

if (Application.isPlaying != true)

{

CurShader = Shader.Find("浅墨Shader编程/Volume8/运动模糊特效标准版");

}

#endif

}

//-----------------------------------------【OnDisable()函数】---------------------------------------

// 说明:当对象变为不可用或非激活状态时此函数便被调用

//--------------------------------------------------------------------------------------------------------

void OnDisable()

{

if (CurMaterial)

{

DestroyImmediate(CurMaterial);

}

}

}


3.3 关于如何使用此特效

使用方面的话比较简单,把脚本文件拖到主摄像机上面,效果就出来了。

脚本文件中有如下这些参数可以调整,得到不同的模糊效果:



Iteration Number- 迭代次数
Intensity - 模糊强度
Offset X - X方向上的偏移
Offset Y - Y方向上的偏移

四、最终的效果展示

这边贴几张场景的效果图和使用了屏幕特效后的效果图。需要注意的是,本次的场景效果,除了类似CS/CF的FPS游戏的控制系统以外,还可以使用键盘上的按键【F】,开启或者关闭运动模糊特效。正如下图所展示的:



下面放几张测试截图。

首先,Unity5中,导出的exe使用了新的片头Logo,质感不错,好评:



城镇中的原始效果:



运动模糊后的效果:



海港原始效果:



海港运动模糊后的效果:



城镇草丛前的效果:



城镇草丛前运动模糊后的效果:



沙滩原始效果:



沙滩运动模糊后的效果:



海面原始效果:



海面运动模糊后的效果:



五、后记

本来准备这次更新再稍微剖析一下Unity5中主推的Standard Shader的写法思路的,但发现这篇博文的篇幅已经有点长了,那么,StandardShader就留到下次更新再讲。

本次的更新大致如此,以后的更新依然是安排在每周一,最近一段时间尽量保证每周都更。

最后,感谢各位捧场,我们下周再见。

附: 本博文相关下载链接清单

【百度云】博文示例场景exe试玩下载

【百度云】博文示例场景所有资源与源码的Unitypackage合集下载

【百度云】【Unity5-Shader模板中文注释格式调整版替换文件】下载

【Github】Unity5新版模板源码逐行注释

【Github】运动模糊效果实现源码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  shader unity unity3d