2016-07-10 15:51:33 zhmxy555 阅读数 56021



本系列文章由@浅墨_毛星云 出品,转载请注明出处。  
文章链接: http://blog.csdn.net/poem_qianmo/article/details/51871531
作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1 


 

本篇文章将分析如何在Unity中基于Shader实现高斯模糊屏幕后期特效。

首先放出最终的实现效果。如下几幅图,是在Unity中使用本文所实现的Shader得到的高斯模糊屏幕后期特效与原始图的效果对比图。

 

 

卡通风格的效果测试:



 


写实风格的效果测试:






OK,下面我们开始分析如何在Unity中实现上述的高斯模糊特效。

 




 

一、降采样与高斯模糊的原理


 

首先梳理一下在Unity中实现高斯模糊效果需用到的几个图像处理的知识点,说起来也很巧,正好和之前我写过一个关于OpenCV的系列博客里的这篇文章(http://blog.csdn.net/poem_qianmo/article/details/22745559涉及的知识点类似。

 

 



1.1 关于图像的降采样

 


降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率。对于一幅N*M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。

不难得出,降采样系数K值越大,则需要处理的像素点越少,运行速度越快。




1.2 高斯模糊的原理

 


高斯模糊(Gaussian Blur),也叫高斯平滑,高斯滤波,其通常用它来减少图像噪声以及降低细节层次,常常也被用于对图像进行模糊。

通俗的讲,高斯模糊就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯模糊的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

高斯分布的数学表示如下:

其中,x为到像素中心的距离,σ为标准差。

 

 

高斯分布(正态分布曲线)

分条来说明一下高斯模糊的几个要点:

  • 从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。
  • 由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。
  • 高斯模糊能够把某一点周围的像素色值按高斯曲线统计起来,采用数学上加权平均的计算方法得到这条曲线的色值
  • 所谓"模糊",可以理解成每一个像素都取周边像素的平均值。
  • 图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。

高斯模糊的原理大致如此。若各位还想进一步了解,可以参考高斯模糊的wiki,以及《Real-Time Rendering 3rd》,或各种图像处理的书籍。相关参考内容见附录中的reference。

下面主要来一起看一下高斯模糊特效在Unity中的实现。


 




二、高斯模糊特效在Unity中的实现





Unity中的屏幕特效,通常分为两部分来实现:

    • Shader代码实现部分
    • C#/javascript代码实现部分

 上述两者结合起来,便可以在Unity中实现具有很强可控性和灵活性的屏幕后期特效。

下面即是从这两个方面对高斯模糊的特效进行实现。其实现思路类似Standard Assets/Image Effect中的Blur,但是本文的实现更简洁,有更大的可控性。

 

 


2.1 Shader代码部分



本次的高斯模糊Shader包含逐行注释后约200多行。

书写思路方面,采用了3个通道(Pass)各司其职,他们分别是:

  • 通道0:降采样通道。
  • 通道1:垂直方向模糊处理通道。
  • 通道2:水平方向模糊处理通道。

而三个通道中共用的变量、函数和结构体的代码位于CGINCLUDE和ENDCG之间。

以下贴出经过详细注释的Shader源码:

 

Shader "Learning Unity Shader/Lecture 15/RapidBlurEffect"
{
	//-----------------------------------【属性 || Properties】------------------------------------------  
	Properties
	{
		//主纹理
		_MainTex("Base (RGB)", 2D) = "white" {}
	}

	//----------------------------------【子着色器 || SubShader】---------------------------------------  
	SubShader
	{
		ZWrite Off
		Blend Off

		//---------------------------------------【通道0 || Pass 0】------------------------------------
		//通道0:降采样通道 ||Pass 0: Down Sample Pass
		Pass
		{
			ZTest Off
			Cull Off

			CGPROGRAM

			//指定此通道的顶点着色器为vert_DownSmpl
			#pragma vertex vert_DownSmpl
			//指定此通道的像素着色器为frag_DownSmpl
			#pragma fragment frag_DownSmpl

			ENDCG

		}

		//---------------------------------------【通道1 || Pass 1】------------------------------------
		//通道1:垂直方向模糊处理通道 ||Pass 1: Vertical Pass
		Pass
		{
			ZTest Always
			Cull Off

			CGPROGRAM

			//指定此通道的顶点着色器为vert_BlurVertical
			#pragma vertex vert_BlurVertical
			//指定此通道的像素着色器为frag_Blur
			#pragma fragment frag_Blur

			ENDCG
		}

		//---------------------------------------【通道2 || Pass 2】------------------------------------
		//通道2:水平方向模糊处理通道 ||Pass 2: Horizontal Pass
		Pass
		{
			ZTest Always
			Cull Off

			CGPROGRAM

			//指定此通道的顶点着色器为vert_BlurHorizontal
			#pragma vertex vert_BlurHorizontal
			//指定此通道的像素着色器为frag_Blur
			#pragma fragment frag_Blur

			ENDCG
		}
	}


	//-------------------------CG着色语言声明部分 || Begin CG Include Part----------------------  
	CGINCLUDE

	//【1】头文件包含 || include
	#include "UnityCG.cginc"

	//【2】变量声明 || Variable Declaration
	sampler2D _MainTex;
	//UnityCG.cginc中内置的变量,纹理中的单像素尺寸|| it is the size of a texel of the texture
	uniform half4 _MainTex_TexelSize;
	//C#脚本控制的变量 || Parameter
	uniform half _DownSampleValue;

	//【3】顶点输入结构体 || Vertex Input Struct
	struct VertexInput
	{
		//顶点位置坐标
		float4 vertex : POSITION;
		//一级纹理坐标
		half2 texcoord : TEXCOORD0;
	};

	//【4】降采样输出结构体 || Vertex Input Struct
	struct VertexOutput_DownSmpl
	{
		//像素位置坐标
		float4 pos : SV_POSITION;
		//一级纹理坐标(右上)
		half2 uv20 : TEXCOORD0;
		//二级纹理坐标(左下)
		half2 uv21 : TEXCOORD1;
		//三级纹理坐标(右下)
		half2 uv22 : TEXCOORD2;
		//四级纹理坐标(左上)
		half2 uv23 : TEXCOORD3;
	};


	//【5】准备高斯模糊权重矩阵参数7x4的矩阵 ||  Gauss Weight
	static const half4 GaussWeight[7] =
	{
		half4(0.0205,0.0205,0.0205,0),
		half4(0.0855,0.0855,0.0855,0),
		half4(0.232,0.232,0.232,0),
		half4(0.324,0.324,0.324,1),
		half4(0.232,0.232,0.232,0),
		half4(0.0855,0.0855,0.0855,0),
		half4(0.0205,0.0205,0.0205,0)
	};


	//【6】顶点着色函数 || Vertex Shader Function
	VertexOutput_DownSmpl vert_DownSmpl(VertexInput v)
	{
		//【6.1】实例化一个降采样输出结构
		VertexOutput_DownSmpl o;

		//【6.2】填充输出结构
		//将三维空间中的坐标投影到二维窗口  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//对图像的降采样:取像素上下左右周围的点,分别存于四级纹理坐标中
		o.uv20 = v.texcoord + _MainTex_TexelSize.xy* half2(0.5h, 0.5h);;
		o.uv21 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, -0.5h);
		o.uv22 = v.texcoord + _MainTex_TexelSize.xy * half2(0.5h, -0.5h);
		o.uv23 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, 0.5h);

		//【6.3】返回最终的输出结果
		return o;
	}

	//【7】片段着色函数 || Fragment Shader Function
	fixed4 frag_DownSmpl(VertexOutput_DownSmpl i) : SV_Target
	{
		//【7.1】定义一个临时的颜色值
		fixed4 color = (0,0,0,0);

		//【7.2】四个相邻像素点处的纹理值相加
		color += tex2D(_MainTex, i.uv20);
		color += tex2D(_MainTex, i.uv21);
		color += tex2D(_MainTex, i.uv22);
		color += tex2D(_MainTex, i.uv23);

		//【7.3】返回最终的平均值
		return color / 4;
	}

	//【8】顶点输入结构体 || Vertex Input Struct
	struct VertexOutput_Blur
	{
		//像素坐标
		float4 pos : SV_POSITION;
		//一级纹理(纹理坐标)
		half4 uv : TEXCOORD0;
		//二级纹理(偏移量)
		half2 offset : TEXCOORD1;
	};

	//【9】顶点着色函数 || Vertex Shader Function
	VertexOutput_Blur vert_BlurHorizontal(VertexInput v)
	{
		//【9.1】实例化一个输出结构
		VertexOutput_Blur o;

		//【9.2】填充输出结构
		//将三维空间中的坐标投影到二维窗口  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//纹理坐标
		o.uv = half4(v.texcoord.xy, 1, 1);
		//计算X方向的偏移量
		o.offset = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _DownSampleValue;

		//【9.3】返回最终的输出结果
		return o;
	}

	//【10】顶点着色函数 || Vertex Shader Function
	VertexOutput_Blur vert_BlurVertical(VertexInput v)
	{
		//【10.1】实例化一个输出结构
		VertexOutput_Blur o;

		//【10.2】填充输出结构
		//将三维空间中的坐标投影到二维窗口  
		o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
		//纹理坐标
		o.uv = half4(v.texcoord.xy, 1, 1);
		//计算Y方向的偏移量
		o.offset = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _DownSampleValue;

		//【10.3】返回最终的输出结果
		return o;
	}

	//【11】片段着色函数 || Fragment Shader Function
	half4 frag_Blur(VertexOutput_Blur i) : SV_Target
	{
		//【11.1】获取原始的uv坐标
		half2 uv = i.uv.xy;

		//【11.2】获取偏移量
		half2 OffsetWidth = i.offset;
		//从中心点偏移3个间隔,从最左或最上开始加权累加
		half2 uv_withOffset = uv - OffsetWidth * 3.0;

		//【11.3】循环获取加权后的颜色值
		half4 color = 0;
		for (int j = 0; j< 7; j++)
		{
			//偏移后的像素纹理值
			half4 texCol = tex2D(_MainTex, uv_withOffset);
			//待输出颜色值+=偏移后的像素纹理值 x 高斯权重
			color += texCol * GaussWeight[j];
			//移到下一个像素处,准备下一次循环加权
			uv_withOffset += OffsetWidth;
		}

		//【11.4】返回最终的颜色值
		return color;
	}

	//-------------------结束CG着色语言声明部分  || End CG Programming Part------------------  			
	ENDCG

	FallBack Off
}





2.2 C#代码部分



C#脚本文件的代码可以从我们之前的几篇分析屏幕特效实现的文章中重用(如这篇实现屏幕油画特效的文章http://blog.csdn.net/poem_qianmo/article/details/49719247),只用稍微改一点细节即可。 

贴出详细注释的配合Shader实现此特效的C#脚本:

using UnityEngine;
using System.Collections;

//设置在编辑模式下也执行该脚本
[ExecuteInEditMode]
//添加选项到菜单中
[AddComponentMenu("Learning Unity Shader/Lecture 15/RapidBlurEffect")]
public class RapidBlurEffect : MonoBehaviour
{
    //-------------------变量声明部分-------------------
    #region Variables
    
    //指定Shader名称
    private string ShaderName = "Learning Unity Shader/Lecture 15/RapidBlurEffect";

    //着色器和材质实例
    public Shader CurShader;
    private Material CurMaterial;

    //几个用于调节参数的中间变量
    public static int ChangeValue;
    public static float ChangeValue2;
    public static int ChangeValue3;

    //降采样次数
    [Range(0, 6), Tooltip("[降采样次数]向下采样的次数。此值越大,则采样间隔越大,需要处理的像素点越少,运行速度越快。")]
    public int DownSampleNum = 2;
    //模糊扩散度
    [Range(0.0f, 20.0f), Tooltip("[模糊扩散度]进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。")]
    public float BlurSpreadSize = 3.0f;
    //迭代次数
    [Range(0, 8), Tooltip("[迭代次数]此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大。")]
    public int BlurIterations = 3;

    #endregion

    //-------------------------材质的get&set----------------------------
    #region MaterialGetAndSet
    Material material
    {
        get
        {
            if (CurMaterial == null)
            {
                CurMaterial = new Material(CurShader);
                CurMaterial.hideFlags = HideFlags.HideAndDontSave;
            }
            return CurMaterial;
        }
    }
    #endregion

    #region Functions
    //-----------------------------------------【Start()函数】---------------------------------------------  
    // 说明:此函数仅在Update函数第一次被调用前被调用
    //--------------------------------------------------------------------------------------------------------
    void Start()
    {
        //依次赋值
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;

        //找到当前的Shader文件
        CurShader = Shader.Find(ShaderName);

        //判断当前设备是否支持屏幕特效
        if (!SystemInfo.supportsImageEffects)
        {
            enabled = false;
            return;
        }
    }

    //-------------------------------------【OnRenderImage()函数】------------------------------------  
    // 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果
    //--------------------------------------------------------------------------------------------------------
    void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
    {
        //着色器实例不为空,就进行参数设置
        if (CurShader != null)
        {
            //【0】参数准备
            //根据向下采样的次数确定宽度系数。用于控制降采样后相邻像素的间隔
            float widthMod = 1.0f / (1.0f * (1 << DownSampleNum));
            //Shader的降采样参数赋值
            material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod);
            //设置渲染模式:双线性
            sourceTexture.filterMode = FilterMode.Bilinear;
            //通过右移,准备长、宽参数值
            int renderWidth = sourceTexture.width >> DownSampleNum;
            int renderHeight = sourceTexture.height >> DownSampleNum;

            // 【1】处理Shader的通道0,用于降采样 ||Pass 0,for down sample
            //准备一个缓存renderBuffer,用于准备存放最终数据
            RenderTexture renderBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
            //设置渲染模式:双线性
            renderBuffer.filterMode = FilterMode.Bilinear;
            //拷贝sourceTexture中的渲染数据到renderBuffer,并仅绘制指定的pass0的纹理数据
            Graphics.Blit(sourceTexture, renderBuffer, material, 0);

            //【2】根据BlurIterations(迭代次数),来进行指定次数的迭代操作
            for (int i = 0; i < BlurIterations; i++)
            {
                //【2.1】Shader参数赋值
                //迭代偏移量参数
                float iterationOffs = (i * 1.0f);
                //Shader的降采样参数赋值
                material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod + iterationOffs);

                // 【2.2】处理Shader的通道1,垂直方向模糊处理 || Pass1,for vertical blur
                // 定义一个临时渲染的缓存tempBuffer
                RenderTexture tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass1的纹理数据
                Graphics.Blit(renderBuffer, tempBuffer, material, 1);
                //  清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0和pass1的数据已经准备好
                 renderBuffer = tempBuffer;

                // 【2.3】处理Shader的通道2,竖直方向模糊处理 || Pass2,for horizontal blur
                // 获取临时渲染纹理
                tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass2的纹理数据
                Graphics.Blit(renderBuffer, tempBuffer, CurMaterial, 2);

                //【2.4】得到pass0、pass1和pass2的数据都已经准备好的renderBuffer
                // 再次清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 再次将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0、pass1和pass2的数据都已经准备好
                renderBuffer = tempBuffer;
            }

            //拷贝最终的renderBuffer到目标纹理,并绘制所有通道的纹理到屏幕
            Graphics.Blit(renderBuffer, destTexture);
            //清空renderBuffer
            RenderTexture.ReleaseTemporary(renderBuffer);

        }

        //着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的
        else
        {
            //直接拷贝源纹理到目标渲染纹理
            Graphics.Blit(sourceTexture, destTexture);
        }
    }


    //-----------------------------------------【OnValidate()函数】--------------------------------------  
    // 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用
    //--------------------------------------------------------------------------------------------------------
    void OnValidate()
    {
        //将编辑器中的值赋值回来,确保在编辑器中值的改变立刻让结果生效
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;
    }

    //-----------------------------------------【Update()函数】--------------------------------------  
    // 说明:此函数每帧都会被调用
    //--------------------------------------------------------------------------------------------------------
    void Update()
    {
        //若程序在运行,进行赋值
        if (Application.isPlaying)
        {
            //赋值
            DownSampleNum = ChangeValue;
            BlurSpreadSize = ChangeValue2;
            BlurIterations = ChangeValue3;
        }
        //若程序没有在运行,去寻找对应的Shader文件
#if UNITY_EDITOR
        if (Application.isPlaying != true)
        {
            CurShader = Shader.Find(ShaderName);
        }
#endif

    }

    //-----------------------------------------【OnDisable()函数】---------------------------------------  
    // 说明:当对象变为不可用或非激活状态时此函数便被调用  
    //--------------------------------------------------------------------------------------------------------
    void OnDisable()
    {
        if (CurMaterial)
        {
            //立即销毁材质实例
            DestroyImmediate(CurMaterial);
        }

    }

 #endregion

}


将此C#代码拖拽到场景的主摄像机之上, 且你的工程中也存在2.1节中贴出的Shader代码,那么就可以在Game窗口中看到经过了屏幕模糊特效的处理后的镜头效果。

 

而Inspector中可得到如下所示的脚本选项。

 

其中,有3个选项可以调节,他们分别是:

  • [Down Sample Num] – 降采样的次数。此值越大,则采样间隔越大,需要处理的像素点越少,运行速度越快。
  • [Blur Speread Size] -模糊扩散度。进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。
  • [Blur Iterations] -迭代次数。此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大。

调节这三个参数,便可以在场景中定制出自己需要的模糊特效。

 




2.3 推荐几组参数设置



这边推荐几组效果出色较为出色的参数预设,方便有需要的朋友定制出适合自己的效果。

 





 




 


 


三、最终实现的效果图示




3.1 Low Poly风格的效果测试



 







3.2 卡通风格效果测试


 


 


 

 


3.3 写实风格的效果测试


 

 




 

 

 

附1、本文配套源码下载链接


  【Github】本文Shader源码

 


附2、Reference

 

[1] https://en.wikipedia.org/wiki/Gaussian_blur

[2] http://www.cnblogs.com/foxianmo/p/4931507.html

[3]《Real-Time Rendering 3rd》,p467-p473.

 

2015-11-29 13:59:16 zhmxy555 阅读数 22833
 

本系列文章由@浅墨_毛星云 出品,转载请注明出处。  
文章链接: http://blog.csdn.net/poem_qianmo/article/details/50095705
作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1 


 
概要:续接上文,本文进一步讲解与分析了上文未讲完的Unity5中Standard Shader正向基础渲染通道源码的片段着色实现部分,以及对屏幕像素化后期特效进行了实现。
 
同样需要声明的是。本文中对Stardard Shader源码的一些分析,全是浅墨自己通过对Unity Shader内建源码的理解,以及Google之后理解与分析而来。如有解释不妥当之处,还请各位及时指出。
 

依然是附上一组本文配套工程的运行截图之后,便开始我们的正文。


傍晚的野外(with 屏幕像素化特效):
 


傍晚的野外(原始场景):



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

【可运行的本文配套exe游戏场景请点击这里下载】
 
提示:在此游戏场景中按F键可以开关屏幕特效。


 

 
一、关于BRDF(双向反射分布函数)
 

本次源码剖析有涉及BRDF的相关内容,这边简单提一下。
双向反射分布函数(Bidirectional ReflectanceDistribution Function,BRDF)用来定义给定入射方向上的辐射照度(irradiance)如何影响给定出射方向上的辐射率(radiance)。更笼统地说,它描述了入射光线经过某个表面反射后如何在各个出射方向上分布——这可以是从理想镜面反射到漫反射、各向同性(isotropic)或者各向异性(anisotropic)的各种反射。
 
BRDF作为图形学中比较常见的一个知识点,这边暂时不多讲,因为随便拿一本图形学相关的书都可以看到他的身影。这边给出一些参考的链接,大家有需要可以深入了解:
 
1. 如何正确理解 BRDF (双向反射分布函数)? - 计算机 - 知乎
2.图形学理论知识:BRDF 双向反射分布函数
3. An Introduction to BRDF-based Lighting –Nvidia


 



 

二、续Standard Shader中正向基础渲染通道源码分析


 


此部分接上文《【浅墨Unity3D Shader编程】之十 深入理解Unity5中的Standard Shader(二)&屏幕油画特效的实现》的第二部分“Standard Shader中正向基础渲染通道源码分析“。

上文中分析了Standard Shader中正向基础渲染通道的源码,刚好分析完了顶点着色函数vertForwardBase,本文中将对片段着色函数fragForwardBase 进行说明。分析完之后,也就结束了这一系列长得稍微有些离谱的Standard Shader正向基础渲染通道的源码分析。
 

OK,开始吧,先上注释好的片段着色函数fragForwardBase的代码,位于UnityStandardCore.cginc中:

//----------------------------------------【fragForwardBase函数】-------------------------------------------
//  用途:正向渲染基础通道的片段着色函数
//  输入:VertexOutputForwardBase结构体
//  输出:一个half4类型的颜色值
//------------------------------------------------------------------------------------------------------------------
half4 fragForwardBase (VertexOutputForwardBase i) : SV_Target
{
	//定义并初始化类型为FragmentCommonData的变量s
	FRAGMENT_SETUP(s)
	//若定义了UNITY_OPTIMIZE_TEXCUBELOD,则由输入的顶点参数来设置反射光方向向量
#if UNITY_OPTIMIZE_TEXCUBELOD
	s.reflUVW		= i.reflUVW;
#endif

	//设置主光照
	UnityLight mainLight = MainLight (s.normalWorld);

	//设置阴影的衰减系数
	half atten = SHADOW_ATTENUATION(i);

	//计算全局光照
	half occlusion = Occlusion(i.tex.xy);
	UnityGI gi = FragmentGI (s, occlusion, i.ambientOrLightmapUV, atten, mainLight);

	//加上BRDF-基于物理的光照
	half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
	//加上BRDF-全局光照
	c.rgb += UNITY_BRDF_GI (s.diffColor, s.specColor, s.oneMinusReflectivity, s.oneMinusRoughness, s.normalWorld, -s.eyeVec, occlusion, gi);
	//加上自发光
	c.rgb += Emission(i.tex.xy);

	//设置雾效
	UNITY_APPLY_FOG(i.fogCoord, c.rgb);

	//返回最终的颜色
	return OutputForward (c, s.alpha);
}
依然是老规矩,把上面代码中新接触到的相关内容进行下分条讲解。





1. FRAGMENT_SETUP(x)宏

FRAGMENT_SETUP(x)宏定义于UnityStandardCore.cginc头文件中,其作用其实就是用FragmentSetup函数初始化括号中的x变量。
#define FRAGMENT_SETUP(x) FragmentCommonData x = \
	FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));

调用此宏,也就是表示写了如下的代码,定义了一个x变量:
FragmentCommonData x =FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));


其中FragmentSetup函数也定义于UnityStandardCore.cginc头文件中,用于填充一个FragmentCommonData结构体并于返回值中返回,也就是进行片段函数相关参数的初始化,相关代码如下:

//函数FragmentSetup:填充一个FragmentCommonData结构体并于返回值中返回,进行片段函数相关参数的初始化
inline FragmentCommonData FragmentSetup (float4 i_tex, half3 i_eyeVec, half3 i_viewDirForParallax, half4 tangentToWorld[3], half3 i_posWorld)
{
	i_tex = Parallax(i_tex, i_viewDirForParallax);

	half alpha = Alpha(i_tex.xy);
	#if defined(_ALPHATEST_ON)
		clip (alpha - _Cutoff);
	#endif

	FragmentCommonData o = UNITY_SETUP_BRDF_INPUT (i_tex);
	o.normalWorld = PerPixelWorldNormal(i_tex, tangentToWorld);
	o.eyeVec = NormalizePerPixelNormal(i_eyeVec);
	o.posWorld = i_posWorld;

	// NOTE: shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
	o.diffColor = PreMultiplyAlpha (o.diffColor, alpha, o.oneMinusReflectivity, /*out*/ o.alpha);
	return o;
}
其中的FragmentCommonData结构体也是定义于UnityStandardCore.cginc头文件中:
//FragmentCommonData结构体:存放片段着色常用变量
struct FragmentCommonData
{
	half3 diffColor, specColor;//漫反射颜色;镜面反射颜色
	// Note: oneMinusRoughness & oneMinusReflectivity for optimization purposes, mostly for DX9 SM2.0 level.
	// Most of the math is being done on these (1-x) values, and that saves a few precious ALU slots.
	half oneMinusReflectivity, oneMinusRoughness;//1减去反射率;1减去粗糙度
	half3 normalWorld, eyeVec, posWorld;//世界空间中的法线向量坐标;视角向量坐标;在世界坐标中的位置坐标
	half alpha;//透明度

#if UNITY_OPTIMIZE_TEXCUBELOD || UNITY_STANDARD_SIMPLE
	half3 reflUVW;//反射率的UVW
#endif

#if UNITY_STANDARD_SIMPLE
	half3 tangentSpaceNormal;//切线空间中的法线向量
#endif
};



 

2. MainLight函数



MainLight函数定义于UnityStandardCore.cginc头文件中,用途是实例化一个UnityLight结构体对象,并进行相应的填充,其返回值作为主光源。相关代码如下:

//  用途:该函数为主光照函数
//  说明:实例化一个UnityLight结构体对象,并进行相应的填充
/*
//注:UnityLight结构体定义于UnityLightingCommon.cginc文件中,原型如下:
struct UnityLight
{
half3 color;
half3 dir;
half  ndotl;
};
*/

//------------------------------------【函数3】MainLight函数-----------------------------------------
//  用途:该函数为主光照函数
//  说明:实例化一个UnityLight结构体对象,并进行相应的填充
//---------------------------------------------------------------------------------------------------------
UnityLight MainLight (half3 normalWorld)
{
	//【1】实例化一个UnityLight的对象
	UnityLight l;

	//【2】填充UnityLight的各个参数
	//若光照贴图选项为关,使用Unity内置变量赋值
	#ifdef LIGHTMAP_OFF
		//获取光源的颜色
		l.color = _LightColor0.rgb; 
		//获取光源的方向
		l.dir = _WorldSpaceLightPos0.xyz;
		//获取法线与光源方向的点乘的积
		l.ndotl = LambertTerm (normalWorld, l.dir);

	//光照贴图选项为开,将各项值设为0
	#else
		l.color = half3(0.f, 0.f, 0.f);
		l.ndotl  = 0.f;
		l.dir = half3(0.f, 0.f, 0.f);
	#endif

	//返回赋值完成的UnityLight结构体对象
	return l;
}


 

3. SHADOW_ATTENUATION宏



SHADOW_ATTENUATION宏相关的代码位于AutoLight.cginc头文件中,用于实现阴影渲染相关的辅助工作,代码如下:

// ----------------
//  阴影相关工具代码 || Shadow helpers
// ----------------

// ---- 屏幕空间阴影 || Screen space shadows
#if defined (SHADOWS_SCREEN)
……
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
#endif


// ----聚光灯光源阴影 || Spot light shadows
#if defined (SHADOWS_DEPTH) && defined (SPOT)
	#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
	#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));
	#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#endif


// ----点光源阴影 ||  Point light shadows
#if defined (SHADOWS_CUBE)
	#define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;
	#define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz;
	#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#endif

// ---- 关闭阴影 || Shadows off
#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE)
	#define SHADOW_COORDS(idx1)
	#define TRANSFER_SHADOW(a)
	#define SHADOW_ATTENUATION(a) 1.0
#endif

可以发现,SHADOW_ATTENUATION(a)宏除了在关闭阴影的状态是等于1以外,其他几种情况都是等价于UnitySampleShadowmap(a._ShadowCoord)函数的调用。而这里的UnitySampleShadowmap函数,定于于UnityShadowLibrary.cginc函数中。实现代码如下。

//------------------------------【UnitySampleShadowmap函数】---------------------------------
// 用途:采样阴影贴图,得到阴影衰减值
// 输入参数: float3型的阴影向量坐标vec
// 返回值:阴影衰减值
//-------------------------------------------------------------------------------------------------------
inline half UnitySampleShadowmap (float3 vec)
{
	float mydist = length(vec) * _LightPositionRange.w;
	mydist *= 0.97; // bias

	#if defined (SHADOWS_SOFT)
		float z = 1.0/128.0;
		float4 shadowVals;
		shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
		shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z));
		shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z));
		shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z));
		half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
		return dot(shadows,0.25);
	#else
		float dist = SampleCubeDistance (vec);
		return dist < mydist ? _LightShadowData.r : 1.0;
	#endif
}



 

4. Occlusion函数


Occlusion函数用于进行全局光照的第一步。其输入参数为一个float2型的纹理坐标,而其half型的返回值将作为FragmentGI函数的一个输入参数。Occlusion函数的原型如下:

half Occlusion(float2 uv)
{
#if (SHADER_TARGET < 30)
	// SM20: instruction count limitation
	// SM20: simpler occlusion
	return tex2D(_OcclusionMap, uv).g;
#else
	half occ = tex2D(_OcclusionMap, uv).g;
	return LerpOneTo (occ, _OcclusionStrength);
#endif
}

其中的LerpOneTo函数很简单,用于线性插值,输入两个值b和t,返回1+(b-1)*t,具体定义如下:
half LerpOneTo(half b, half t)
{
	half oneMinusT = 1 - t;
	return oneMinusT + b * t;
}



5. UnityGI结构体
 

UnityGI结构体是Unity中存放全局光照光源信息的结构体,定义于UnityLightingCommon.cginc头文件中,如下。

//全局光照结构体
struct UnityGI
{
	UnityLight light;//定义第一个光源参数结构体,表示第一个光源
	//若定义了DIRLIGHTMAP_SEPARATE(单独的方向光源光照贴图)
	#ifdef DIRLIGHTMAP_SEPARATE
		//若定义了LIGHTMAP_ON(打开光照贴图)
		#ifdef LIGHTMAP_ON
			UnityLight light2;//定义第二个光源参数结构体,表示第二个光源
		#endif
		//若定义了DYNAMICLIGHTMAP_ON(打开动态光照贴图)
		#ifdef DYNAMICLIGHTMAP_ON
			UnityLight light3;//定义第三个光源参数结构体,表示第三个光源
		#endif
	#endif
	UnityIndirect indirect;//Unity中间接光源参数的结构体
};

其中包含了UnityLight结构体和UnityIndirect结构体,其中UnityLight结构体是Unity Shader中最基本的光照结构体,而UnityIndirect是Unity中存放间接光源信息的结构体。它们两者也定义于UnityLightingCommon.cginc头文件中,代码如下。

//Unity中光源参数的结构体
struct UnityLight
{
	half3 color;//光源颜色
	half3 dir;//光源方向
	half  ndotl; //入射光方向和当前表面法线方向的点积
};

//Unity中间接光源参数的结构体
struct UnityIndirect
{
	half3 diffuse;//漫反射颜色
	half3 specular;//镜面反射颜色
};




 
6. FragmentGI函数


FragmentGI函数是片段着色部分全局光照的处理函数,定义于UnityStandardCore.cginc头文件中。相关代码如下:

//函数:片段着色部分全局光照的处理函数
inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light, bool reflections)
{
	//【1】实例化一个UnityGIInput的对象
	UnityGIInput d;
	//【2】填充此UnityGIInput对象的各个值
	d.light = light;
	d.worldPos = s.posWorld;
	d.worldViewDir = -s.eyeVec;
	d.atten = atten;
	#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
		d.ambient = 0;
		d.lightmapUV = i_ambientOrLightmapUV;
	#else
		d.ambient = i_ambientOrLightmapUV.rgb;
		d.lightmapUV = 0;
	#endif
	d.boxMax[0] = unity_SpecCube0_BoxMax;
	d.boxMin[0] = unity_SpecCube0_BoxMin;
	d.probePosition[0] = unity_SpecCube0_ProbePosition;
	d.probeHDR[0] = unity_SpecCube0_HDR;

	d.boxMax[1] = unity_SpecCube1_BoxMax;
	d.boxMin[1] = unity_SpecCube1_BoxMin;
	d.probePosition[1] = unity_SpecCube1_ProbePosition;
	d.probeHDR[1] = unity_SpecCube1_HDR;

	//【3】根据填充好的UnityGIInput结构体对象,调用一下UnityGlobalIllumination函数
	if(reflections)
	{
		Unity_GlossyEnvironmentData g;
		g.roughness		= 1 - s.oneMinusRoughness;
	#if UNITY_OPTIMIZE_TEXCUBELOD || UNITY_STANDARD_SIMPLE
		g.reflUVW 		= s.reflUVW;
	#else
		g.reflUVW		= reflect(s.eyeVec, s.normalWorld);
	#endif

		return UnityGlobalIllumination (d, occlusion, s.normalWorld, g);
	}
	else
	{
		return UnityGlobalIllumination (d, occlusion, s.normalWorld);
	}
}

inline UnityGI FragmentGI (FragmentCommonData s, half occlusion, half4 i_ambientOrLightmapUV, half atten, UnityLight light)
{
	return FragmentGI(s, occlusion, i_ambientOrLightmapUV, atten, light, true);
}


其中的UnityGIInput结构体定义了全局光照所需要的一些函数,定义为如下:

//全局光照的输入参数结构体
struct UnityGIInput 
{
	UnityLight light; // 像素光源,由引擎准备并传输过来 || pixel light, sent from the engine

	float3 worldPos;//世界空间中的位置坐标
	half3 worldViewDir;//世界空间中的视角方向向量坐标
	half atten;//衰减值
	half3 ambient;//环境光颜色
	half4 lightmapUV; //光照贴图的UV坐标,其中 取.xy = static lightmapUV(静态光照贴图的UV) , .zw = dynamic lightmap UV(动态光照贴图的UV)

	float4 boxMax[2];//box最大值
	float4 boxMin[2];//box最小值
	float4 probePosition[2];//光照探针的位置
	float4 probeHDR[2];//光照探针的高动态范围图像(High-Dynamic Range)
};

UnityGIInput 中还包含了UnityLight结构体,其定义和代码实现上文刚刚已经有提到过。

FragmentGI函数最终利用了UnityGlobalIllumination函数,其定义于UnityGlobalIllumination.cginc头文件中,实现如下。

inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld)
{
	return UnityGI_Base(data, occlusion, normalWorld);
}


FragmentGI函数就是嵌套了一层UnityGI_Base函数,那我们继续溯源,找到UnityGI_Base函数的定义,也是位于UnityGlobalIllumination.cginc头文件中:
//UnityGI_Base函数:Unity的全局光照Base版
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)
{
	//【1】实例化一个UnityGI类型的结构体
	UnityGI o_gi;
	//【2】重置此UnityGI的结构体
	ResetUnityGI(o_gi);

	//【3】开始逐个填充参数
	#if !defined(LIGHTMAP_ON)
		o_gi.light = data.light;
		o_gi.light.color *= data.atten;
	#endif


	#if UNITY_SHOULD_SAMPLE_SH
		#if UNITY_SAMPLE_FULL_SH_PER_PIXEL
			half3 sh = ShadeSH9(half4(normalWorld, 1.0));
		#elif (SHADER_TARGET >= 30) && !UNITY_STANDARD_SIMPLE
			half3 sh = data.ambient + ShadeSH12Order(half4(normalWorld, 1.0));
		#else
			half3 sh = data.ambient;
		#endif

		o_gi.indirect.diffuse = sh;
	#endif


	#if defined(LIGHTMAP_ON)
		// Baked lightmaps
		fixed4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data.lightmapUV.xy);
		half3 bakedColor = DecodeLightmap(bakedColorTex);

		#ifdef DIRLIGHTMAP_OFF
			o_gi.indirect.diffuse = bakedColor;

			#ifdef SHADOWS_SCREEN
				o_gi.indirect.diffuse = MixLightmapWithRealtimeAttenuation (o_gi.indirect.diffuse, data.atten, bakedColorTex);
			#endif // SHADOWS_SCREEN

		#elif DIRLIGHTMAP_COMBINED
			fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER (unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
			o_gi.indirect.diffuse = DecodeDirectionalLightmap (bakedColor, bakedDirTex, normalWorld);

			#ifdef SHADOWS_SCREEN
				o_gi.indirect.diffuse = MixLightmapWithRealtimeAttenuation (o_gi.indirect.diffuse, data.atten, bakedColorTex);
			#endif // SHADOWS_SCREEN

		#elif DIRLIGHTMAP_SEPARATE
			// Left halves of both intensity and direction lightmaps store direct light; right halves - indirect.

			// Direct
			fixed4 bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, data.lightmapUV.xy);
			o_gi.indirect.diffuse = DecodeDirectionalSpecularLightmap (bakedColor, bakedDirTex, normalWorld, false, 0, o_gi.light);

			// Indirect
			half2 uvIndirect = data.lightmapUV.xy + half2(0.5, 0);
			bakedColor = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, uvIndirect));
			bakedDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uvIndirect);
			o_gi.indirect.diffuse += DecodeDirectionalSpecularLightmap (bakedColor, bakedDirTex, normalWorld, false, 0, o_gi.light2);
		#endif
	#endif

	#ifdef DYNAMICLIGHTMAP_ON
		// Dynamic lightmaps
		fixed4 realtimeColorTex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, data.lightmapUV.zw);
		half3 realtimeColor = DecodeRealtimeLightmap (realtimeColorTex);

		#ifdef DIRLIGHTMAP_OFF
			o_gi.indirect.diffuse += realtimeColor;

		#elif DIRLIGHTMAP_COMBINED
			half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
			o_gi.indirect.diffuse += DecodeDirectionalLightmap (realtimeColor, realtimeDirTex, normalWorld);

		#elif DIRLIGHTMAP_SEPARATE
			half4 realtimeDirTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicDirectionality, unity_DynamicLightmap, data.lightmapUV.zw);
			half4 realtimeNormalTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_DynamicNormal, unity_DynamicLightmap, data.lightmapUV.zw);
			o_gi.indirect.diffuse += DecodeDirectionalSpecularLightmap (realtimeColor, realtimeDirTex, normalWorld, true, realtimeNormalTex, o_gi.light3);
		#endif
	#endif

	o_gi.indirect.diffuse *= occlusion;

	//【4】返回此UnityGI类型的结构体
	return o_gi;
}
不难发现,此FragmentGI函数的实现,也就是实例化一个UnityGIInput结构体对象,然后依次填充了此结构体对象的每个各个参数,最后调用一下基于UnityGI_Base函数的UnityGlobalIllumination函数而已。

 



7. UNITY_BRDF_PBS宏


首先,这边有一段宏,定义于UnityPBSLighting.cginc头文件中,根据不同的情况,将UNITY_BRDF_PBS宏定义为不同版本的UNITY_BRDF_PBS宏——是BRDF3_Unity_PBS、BRDF2_Unity_PBS还是BRDF1_Unity_PBS。

//-------------------------------------------------------------------------------------
// 默认使用BRDF || Default BRDF to use:
#if !defined (UNITY_BRDF_PBS) // 允许显式地在自定义着色器中重写BRDF的实现细节 || allow to explicitly override BRDF in custom shader
	//满足着色目标模型的版本小于Shader Model 3.0,或者是PlayStation 2平台
	#if (SHADER_TARGET < 30) || defined(SHADER_API_PSP2)
		// 为小于SM3.0的着色模型回退为低保真度的BRDF版本 || Fallback to low fidelity one for pre-SM3.0
		#define UNITY_BRDF_PBS BRDF3_Unity_PBS
	#elif defined(SHADER_API_MOBILE)
		// 为移动平台简化的BRDF版本 || Somewhat simplified for mobile
		#define UNITY_BRDF_PBS BRDF2_Unity_PBS
	#else
		//最高特效的SM3、PC平台或者游戏主机平台的BRDF版本 || Full quality for SM3+ PC / consoles
		#define UNITY_BRDF_PBS BRDF1_Unity_PBS
	#endif
#endif
三种情况下,BRDF3_Unity_PBS、BRDF2_Unity_PBS、 BRDF1_Unity_PBS三个函数的参数和返回值都一样,区别仅仅是内部的实现。在这边,以BRDF1_Unity_PBS为例,讲一下参数值的含义。
 
half4 BRDF1_Unity_PBS (half3 diffColor,half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,half3 normal,half3 viewDir,UnityLight light, UnityIndirect gi)
 
第一个参数,half3型的diffColor,表示漫反射颜色的值。
第二个参数,half3型的specColor,表示镜面反射颜色值。
第三个参数,half型的oneMinusReflectivity,表示1减去反射率的值。
第四个参数,half型的oneMinusRoughness,表示1减去粗糙度的值。
第五次参数,half3型的normal,表示法线的方向。
第六个参数,half3型的viewDir,表示视线的方向。

第七个参数,UnityLight型的light,表示Unity中光源参数的结构体,包含half3型的光源颜色color,half3型的光源方向dir,half型的入射光方向和当前表面法线方向的点乘的积ndotl。上文有贴出过其实现代码,都几次提到了,这边就再贴一下。

struct UnityLight
{
	half3 color;//光源颜色
	half3 dir;//光源方向
	half  ndotl; //入射光方向和当前表面法线方向的点积
};
第八个参数,UnityIndirect类型的gi ,一个包含了half3型的漫反射颜色diffuse和half3型的镜面反射颜色specular的光线反射结构体,表示间接光照信息
struct UnityIndirect
{
	half3 diffuse;//漫反射颜色
	half3 specular;//镜面反射颜色
};

下面将三种版本的函数分别贴出来,它们都定义于UnityStandardBRDF.cginc头文件中。


 
7.1 BRDF1_Unity_PBS


//最高特效的SM3、PC平台或者游戏主机平台的BRDF版本 || Full quality for SM3+ PC / consoles
//-------------------------------------------------------------------------------------

// Note: BRDF entry points use oneMinusRoughness (aka "smoothness") and oneMinusReflectivity for optimization
// purposes, mostly for DX9 SM2.0 level. Most of the math is being done on these (1-x) values, and that saves
// a few precious ALU slots.


// Main Physically Based BRDF
// Derived from Disney work and based on Torrance-Sparrow micro-facet model
//
//   BRDF = kD / pi + kS * (D * V * F) / 4
//   I = BRDF * NdotL
//
// * NDF (depending on UNITY_BRDF_GGX):
//  a) Normalized BlinnPhong
//  b) GGX
// * Smith for Visiblity term
// * Schlick approximation for Fresnel
half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,
	half3 normal, half3 viewDir,
	UnityLight light, UnityIndirect gi)
{
	half roughness = 1-oneMinusRoughness;
	half3 halfDir = Unity_SafeNormalize (light.dir + viewDir);

	half nl = light.ndotl;
	half nh = BlinnTerm (normal, halfDir);
	half nv = DotClamped (normal, viewDir);
	half lv = DotClamped (light.dir, viewDir);
	half lh = DotClamped (light.dir, halfDir);

#if UNITY_BRDF_GGX
	half V = SmithGGXVisibilityTerm (nl, nv, roughness);
	half D = GGXTerm (nh, roughness);
#else
	half V = SmithBeckmannVisibilityTerm (nl, nv, roughness);
	half D = NDFBlinnPhongNormalizedTerm (nh, RoughnessToSpecPower (roughness));
#endif

	half nlPow5 = Pow5 (1-nl);
	half nvPow5 = Pow5 (1-nv);
	half Fd90 = 0.5 + 2 * lh * lh * roughness;
	half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5);
	
	// HACK: theoretically we should divide by Pi diffuseTerm and not multiply specularTerm!
	// BUT 1) that will make shader look significantly darker than Legacy ones
	// and 2) on engine side "Non-important" lights have to be divided by Pi to in cases when they are injected into ambient SH
	// NOTE: multiplication by Pi is part of single constant together with 1/4 now

	half specularTerm = max(0, (V * D * nl) * unity_LightGammaCorrectionConsts_PIDiv4);// Torrance-Sparrow model, Fresnel is applied later (for optimization reasons)
	half diffuseTerm = disneyDiffuse * nl;
	
	half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
    half3 color =	diffColor * (gi.diffuse + light.color * diffuseTerm)
                    + specularTerm * light.color * FresnelTerm (specColor, lh)
					+ gi.specular * FresnelLerp (specColor, grazingTerm, nv);

	return half4(color, 1);
}


7.2 BRDF2_Unity_PBS
12

// 为移动平台简化的BRDF版本 || Somewhat simplified for mobile
// Based on Minimalist CookTorrance BRDF
// Implementation is slightly different from original derivation: http://www.thetenthplanet.de/archives/255
//
// * BlinnPhong as NDF
// * Modified Kelemen and Szirmay-Kalos for Visibility term
// * Fresnel approximated with 1/LdotH
half4 BRDF2_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,
	half3 normal, half3 viewDir,
	UnityLight light, UnityIndirect gi)
{
	half3 halfDir = Unity_SafeNormalize (light.dir + viewDir);

	half nl = light.ndotl;
	half nh = BlinnTerm (normal, halfDir);
	half nv = DotClamped (normal, viewDir);
	half lh = DotClamped (light.dir, halfDir);

	half roughness = 1-oneMinusRoughness;
	half specularPower = RoughnessToSpecPower (roughness);
	// Modified with approximate Visibility function that takes roughness into account
	// Original ((n+1)*N.H^n) / (8*Pi * L.H^3) didn't take into account roughness 
	// and produced extremely bright specular at grazing angles

	// HACK: theoretically we should divide by Pi diffuseTerm and not multiply specularTerm!
	// BUT 1) that will make shader look significantly darker than Legacy ones
	// and 2) on engine side "Non-important" lights have to be divided by Pi to in cases when they are injected into ambient SH
	// NOTE: multiplication by Pi is cancelled with Pi in denominator

	half invV = lh * lh * oneMinusRoughness + roughness * roughness; // approx ModifiedKelemenVisibilityTerm(lh, 1-oneMinusRoughness);
	half invF = lh;
	half specular = ((specularPower + 1) * pow (nh, specularPower)) / (unity_LightGammaCorrectionConsts_8 * invV * invF + 1e-4h); // @TODO: might still need saturate(nl*specular) on Adreno/Mali

	// Prevent FP16 overflow on mobiles
#if SHADER_API_GLES || SHADER_API_GLES3
	specular = clamp(specular, 0.0, 100.0);
#endif

	half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
    half3 color =	(diffColor + specular * specColor) * light.color * nl
    				+ gi.diffuse * diffColor
					+ gi.specular * FresnelLerpFast (specColor, grazingTerm, nv);

	return half4(color, 1);
}


 
7.3 BRDF3_Unity_PBS

// 为小于SM3.0的着色模型回退为低保真度的BRDF版本 || Fallback to low fidelity one for pre-SM3.0
// Old school, not microfacet based Modified Normalized Blinn-Phong BRDF
// Implementation uses Lookup texture for performance
//
// * Normalized BlinnPhong in RDF form
// * Implicit Visibility term
// * No Fresnel term
//
// TODO: specular is too weak in Linear rendering mode
half4 BRDF3_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness,
	half3 normal, half3 viewDir,
	UnityLight light, UnityIndirect gi)
{
	half3 reflDir = reflect (viewDir, normal);

	half nl = light.ndotl;
	half nv = DotClamped (normal, viewDir);

	// Vectorize Pow4 to save instructions
	half2 rlPow4AndFresnelTerm = Pow4 (half2(dot(reflDir, light.dir), 1-nv));  // use R.L instead of N.H to save couple of instructions
	half rlPow4 = rlPow4AndFresnelTerm.x; // power exponent must match kHorizontalWarpExp in NHxRoughness() function in GeneratedTextures.cpp
	half fresnelTerm = rlPow4AndFresnelTerm.y;

	half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));

	half3 color = BRDF3_Direct(diffColor, specColor, rlPow4, oneMinusRoughness);
	color *= light.color * nl;
	color += BRDF3_Indirect(diffColor, specColor, gi, grazingTerm, fresnelTerm);

	return half4(color, 1);
}


BRDF1_Unity_PBS函数的实现部分用到了最多的变量,最终表现效果最好,主要用于Shader Model 3.0、PC平台或者游戏主机平台。BRDF2_Unity_PBS简化了一部分计算,主要用于移动平台,而BRDF3_Unity_PBS是为Shader Model 小于3.0的着色模型提供基本版的BRDF,实现细节最为简陋。


 



8. UNITY_BRDF_GI宏



UNITY_BRDF_GI宏位于UnityPBSLighting.cginc头文件中,相关代码如下。

//-------------------------------------------------------------------------------------
// 从间接的方向光照贴图中进行BRDF(双向反射分布函数)的光照提取 || BRDF for lights extracted from *indirect* directional lightmaps (baked and realtime).
// 使用UNITY_BRDF_PBS从方向光源烘焙方向光照贴图, || Baked directional lightmap with *direct* light uses UNITY_BRDF_PBS.
// 若想得到更好的效果,可以使用BRDF1_Unity_PBS || For better quality change to BRDF1_Unity_PBS.
// SM2.0中的非方向光照贴图|| No directional lightmaps in SM2.0.

//若没有定义UNITY_BRDF_PBS_LIGHTMAP_INDIRECT宏
#if !defined(UNITY_BRDF_PBS_LIGHTMAP_INDIRECT)
	//定义UNITY_BRDF_PBS_LIGHTMAP_INDIRECT = BRDF2_Unity_PBS
	#define UNITY_BRDF_PBS_LIGHTMAP_INDIRECT BRDF2_Unity_PBS
#endif
//若没有定义UNITY_BRDF_GI宏
#if !defined (UNITY_BRDF_GI)
	//定义UNITY_BRDF_GI = BRDF_Unity_Indirect
	#define UNITY_BRDF_GI BRDF_Unity_Indirect
#endif

上面这段代码中关于UNITY_BRDF_GI宏的地方,就是说若没有定义UNITY_BRDF_GI宏,就定义一个UNITY_BRDF_GI宏等价于BRDF_Unity_Indirect。这边的BRDF_Unity_Indirect是一个函数名,就紧紧跟在上面这段宏代码的后面:
 
//间接光照的BRDF
inline half3 BRDF_Unity_Indirect (half3 baseColor, half3 specColor, half oneMinusReflectivity, half oneMinusRoughness, half3 normal, half3 viewDir, half occlusion, UnityGI gi)
{
	half3 c = 0;
	#if defined(DIRLIGHTMAP_SEPARATE)
		gi.indirect.diffuse = 0;
		gi.indirect.specular = 0;

		#ifdef LIGHTMAP_ON
			c += UNITY_BRDF_PBS_LIGHTMAP_INDIRECT (baseColor, specColor, oneMinusReflectivity, oneMinusRoughness, normal, viewDir, gi.light2, gi.indirect).rgb * occlusion;
		#endif
		#ifdef DYNAMICLIGHTMAP_ON
			c += UNITY_BRDF_PBS_LIGHTMAP_INDIRECT (baseColor, specColor, oneMinusReflectivity, oneMinusRoughness, normal, viewDir, gi.light3, gi.indirect).rgb * occlusion;
		#endif
	#endif
	return c;
}

关于此段代码,BRDF_Unity_Indirect 函数的核心部分其实就是在调用UNITY_BRDF_PBS_LIGHTMAP_INDIRECT,而上文的宏有交代过,UNITY_BRDF_PBS_LIGHTMAP_INDIRECT宏等价于 BRDF2_Unity_PBS。而BRDF2_Unity_PBS函数,其定义于UnityStandardBRDF.cginc中,是为移动平台简化的BRDF版本,这个上文刚刚提到过,这边就不多交代。
 

 


 
9.Emission函数


Emission函数定于于UnityStandardInput.cginc头文件中,根据指定的自发光光照贴图,利用tex2D函数,对输入的纹理进行光照贴图的采样,相关代码如下:

//---------------------------------------【Emission函数】-----------------------------------------
// 用途:根据指定的自发光光照贴图,利用tex2D函数,对输入的纹理进行光照贴图的采样
// 输入参数:float2型的纹理坐标
// 输出参数:经过将自发光纹理和输入纹理进行tex2D采样得到的half3型的自发光颜色
//-----------------------------------------------------------------------------------------------
half3 Emission(float2 uv)
{
#ifndef _EMISSION
	return 0;
#else
	return tex2D(_EmissionMap, uv).rgb * _EmissionColor.rgb;
#endif
}

其中用于采样的自发光贴图对应的函数定义于UnityStandardInput.cginc头文件的一开始部分。
sampler2D	_EmissionMap;

这边这句代码其实是相当于在CGPROGRAM中的顶点和片段着色函数之前,对这个变量进行声明,以便于CG语言块中使用的时候,能识别到他的含义。因为在Standard.shader源码的一开始,Properties块也就是属性值声明部分,对其进行了属性的声明:
//自发光纹理图
_EmissionMap("Emission", 2D) = "white" {}


 
 
10.UNITY_APPLY_FOG宏
 


UNITY_APPLY_FOG宏相关的一些代码用于雾效的启用与否的辅助工作,定义于UnityCG.cginc头文件中,这边贴出注释好的代码即可。

//UNITY_FOG_LERP_COLOR宏的定义
#define UNITY_FOG_LERP_COLOR(col,fogCol,fogFac) col.rgb = lerp((fogCol).rgb, (col).rgb, saturate(fogFac))

//【1】若已经定义了FOG_LINEAR、FOG_EXP、FOG_EXP2宏三者至少之一,便可以进行到此#if实现部分
#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
	//【1-1】若满足着色目标模型的版本小于Shader Model 3.0,或者定义了SHADER_API_MOBILE宏,便可以进行到此#if实现部分
	#if (SHADER_TARGET < 30) || defined(SHADER_API_MOBILE)
		//移动平台和Shader Model 2.0:已经计算了每顶点的雾效因子,所以插一下值就可以了 ||mobile or SM2.0: fog factor was already calculated per-vertex, so just lerp the color
		//定义 UNITY_APPLY_FOG_COLOR(coord,col,fogCol) 等价于UNITY_FOG_LERP_COLOR(col,fogCol,coord)
	#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_FOG_LERP_COLOR(col,fogCol,coord)

	//【1-2】 Shader Model 3.0和PC/游戏主机平台:计算雾效因子以及进行雾颜色的插值 ||SM3.0 and PC/console: calculate fog factor and lerp fog color
	#else
		//定义 UNITY_APPLY_FOG_COLOR(coord,col,fogCol)等价于UNITY_CALC_FOG_FACTOR(coord); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor)
		#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol) UNITY_CALC_FOG_FACTOR(coord); UNITY_FOG_LERP_COLOR(col,fogCol,unityFogFactor)
	#endif
//【2】否则,直接定义UNITY_APPLY_FOG_COLOR宏
#else
	#define UNITY_APPLY_FOG_COLOR(coord,col,fogCol)
#endif
//【3】若定义了UNITY_PASS_FORWARDADD(正向附加渲染通道)宏
#ifdef UNITY_PASS_FORWARDADD
	//定义UNITY_APPLY_FOG(coord,col) 等价于UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0))
	#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,fixed4(0,0,0,0))
//【4】否则,UNITY_APPLY_FOG(coord,col) 等价于 UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)
#else
	#define UNITY_APPLY_FOG(coord,col) UNITY_APPLY_FOG_COLOR(coord,col,unity_FogColor)
#endif



 
11.OutputForward函数

OutputForward函数定义于UnityStandardCore.cginc头文件中,其为正向渲染通道的输出函数。
//-----------------------------【函数OutputForward】----------------------------------------------
// 用途:正向渲染通道输出函数
//	输入参数:一个half4类型的一个颜色值output,一个half型的透明度值alphaFromSurface
// 返回值:经过透明处理的half4型的输出颜色值
//-------------------------------------------------------------------------------------------------
half4 OutputForward (half4 output, half alphaFromSurface)
{
	#if defined(_ALPHABLEND_ON) || defined(_ALPHAPREMULTIPLY_ON)
		output.a = alphaFromSurface;
	#else
		UNITY_OPAQUE_ALPHA(output.a);
	#endif
	return output;
}

其中UNITY_OPAQUE_ALPHA宏的定义为:

#define UNITY_OPAQUE_ALPHA(outputAlpha) outputAlpha = 1.0



 
 
三、屏幕像素化特效的实现
 
 

我们都知道,Unity中的屏幕特效通常分为两部分来实现:

  • Shader实现部分
  • 脚本实现部分
下面依旧是从这两个方面对本次的特效进行实现。
 
 
3.1 Shader实现部分
 
 
国际惯例,上注释好的Shader代码。
Shader "浅墨Shader编程/Volume11/PixelEffect"
{
	//------------------------------------【属性值】------------------------------------
	Properties
	{
	//主纹理
	_MainTex("Texture", 2D) = "white" {}
	//封装的变量值
	_Params("PixelNumPerRow (X) Ratio (Y)", Vector) = (80, 1, 1, 1.5)
}

	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//关闭剔除操作
		Cull Off
		//关闭深度写入模式
		ZWrite Off
		//设置深度测试模式:渲染所有像素.等同于关闭透明度测试(AlphaTest Off)
		ZTest Always

		//--------------------------------唯一的通道-------------------------------
		Pass
		{
			//===========开启CG着色器语言编写模块===========
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//包含头文件
			#include "UnityCG.cginc"

			//顶点着色器输入结构
			struct vertexInput
			{
				float4 vertex : POSITION;//顶点位置
				float2 uv : TEXCOORD0;//一级纹理坐标
			};

			//顶点着色器输出结构
			struct vertexOutput
			{
				float4 vertex : SV_POSITION;//像素位置
				float2 uv : TEXCOORD0;//一级纹理坐标
			};

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:顶点输入结构体
			// 输出:顶点输出结构体
			//---------------------------------------------------------------------------------
			//顶点着色函数
			vertexOutput vert(vertexInput   v)
			{
				//【1】实例化一个输入结构体
				vertexOutput o;
				//【2】填充此输出结构
				//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				//输入的UV纹理坐标为顶点输出的坐标
				o.uv = v.uv;

				//【3】返回此输出结构对象
				return o;
			}

			//变量的声明
			sampler2D _MainTex;
			half4 _Params;

			//进行像素化操作的自定义函数PixelateOperation
			half4 PixelateOperation(sampler2D tex, half2 uv, half scale, half ratio)
			{
				//【1】计算每个像素块的尺寸
				half PixelSize = 1.0 / scale;
				//【2】取整计算每个像素块的坐标值,ceil函数,对输入参数向上取整
				half coordX=PixelSize * ceil(uv.x / PixelSize);
				half coordY = (ratio * PixelSize)* ceil(uv.y / PixelSize / ratio);
				//【3】组合坐标值
				half2 coord = half2(coordX,coordY);
				//【4】返回坐标值
				return half4(tex2D(tex, coord).xyzw);
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:顶点输出结构体
			// 输出:float4型的像素颜色值
			//---------------------------------------------------------------------------------
			fixed4 frag(vertexOutput  Input) : COLOR
			{
				//使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值
				return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y);
			}

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

如Shader代码中所展示的,本次的屏幕像素化特效主要用一个自定义函数来实现,实现代码如下:
//进行像素化操作的自定义函数PixelateOperation
			half4 PixelateOperation(sampler2D tex, half2 uv, half scale, half ratio)
			{
				//【1】计算每个像素块的尺寸
				half PixelSize = 1.0 / scale;
				//【2】取整计算每个像素块的坐标值,ceil函数,对输入参数向上取整
				half coordX=PixelSize * ceil(uv.x / PixelSize);
				half coordY=( ratio * PixelSize ) * ceil(uv.y / PixelSize / ratio);
				//【3】组合坐标值
				half2 coord = half2(coordX,coordY);
				//【4】返回坐标值
				return half4(tex2D(tex, coord).xyzw);
			}

首先需要了解到的是,此自定义函数中用到了CG标准函数库中的一个库函数——ceil。ceil(x)的作用是对输入参数向上取整。例如:ceil(float(1.3)) ,返回值就为2.0。
 
PixelateOperation函数的首先先计算出每个像素块的尺寸,然后根据这里的向上取整函数ceil,分别表示出像素块的坐标值。X坐标值为PixelSize * ceil(uv.x / PixelSize)。而Y轴这边还引入了一个系数ratio,先在式子一开头乘以此系数,然后在ceil函数之中的分母部分除以一个ratio,以达到用此参数实现自定义像素长宽比的调整操作。
 

然后在片段着色器中调用此自定义的PixelateOperation函数,其返回值就作为片段函数frag的返回值即可:

		fixed4 frag(vertexOutput  Input) : COLOR
			{
				//使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值
				return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y);
			}



       

        

 
3.2 C#脚本实现部分
 


C#脚本文件的代码依然是几乎从之前的几个特效中重用,只用稍微改一点细节就可以。贴出详细注释的实现此特效的C#脚本:

using UnityEngine;
using System.Collections;

//设置在编辑模式下也执行该脚本
[ExecuteInEditMode]
//添加选项到菜单中
[AddComponentMenu("浅墨Shader编程/Volume11/PixelEffect")]
public class PixelEffect : MonoBehaviour 
{
    //-----------------------------变量声明部分---------------------------
	#region Variables

    //着色器和材质实例
	public Shader CurShader;
	private Material CurMaterial;

    //三个可调节的自定义参数
    [Range(1f, 1024f), Tooltip("屏幕每行将被均分为多少个像素块")]
    public float PixelNumPerRow = 580.0f;

    [Tooltip("自动计算平方像素所需的长宽比与否")]
    public bool AutoCalulateRatio = true;

    [Range(0f, 24f), Tooltip("此参数用于自定义长宽比")]
    public float Ratio = 1.0f;

	#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 () 
	{
        //找到当前的Shader文件
        CurShader = Shader.Find("浅墨Shader编程/Volume11/PixelEffect");

        //判断当前设备是否支持屏幕特效
		if(!SystemInfo.supportsImageEffects)
		{
			enabled = false;
			return;
		}
	}

    //-------------------------------------【OnRenderImage()函数】------------------------------------  
    // 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果
    //--------------------------------------------------------------------------------------------------------
	void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
	{
        //着色器实例不为空,就进行参数设置
        if(CurShader != null)
		{
            float pixelNumPerRow = PixelNumPerRow;
            //给Shader中的外部变量赋值
            material.SetVector("_Params", new Vector2(pixelNumPerRow, 
                AutoCalulateRatio ? ((float)sourceTexture.width / (float)sourceTexture.height) : Ratio ));

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

        //着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的
        else
        {
            //直接拷贝源纹理到目标渲染纹理
            Graphics.Blit(sourceTexture, destTexture);
        }
	}

    //-----------------------------------------【Update()函数】----------------------------------------
    // 说明:此函数在每一帧中都会被调用  
    //------------------------------------------------------------------------------------------------------
    void Update()
    {
        //若程序在运行,进行赋值
        if (Application.isPlaying)
        {
         #if UNITY_EDITOR
            if (Application.isPlaying != true)
            {
                CurShader = Shader.Find("浅墨Shader编程/Volume11/PixelEffect");
            }
        #endif
        }
    }
    //-----------------------------------------【OnDisable()函数】---------------------------------------  
    // 说明:当对象变为不可用或非激活状态时此函数便被调用  
    //--------------------------------------------------------------------------------------------------------
	void OnDisable ()
	{
		if(CurMaterial)
		{
            //立即销毁材质实例
			DestroyImmediate(CurMaterial);	
		}		
	}
}
根据我们C#脚本中参数的设定,可以有每行每列的像素个数PixelNumPerRow参数、是否自动计算正方形像素所需的长宽比与否AutoCalulateRatio参数、自定义长宽比的Ratio参数可以调节。而需要注意,若AutoCalulateRatio参数被勾选,我们的Shader将自动计算正方形像素所需的长宽比,这样第三个参数Ratio也就失效了。反正,若AutoCalulateRatio参数没有被勾选,就可以用Ratio参数自己定制像素的长宽比。

 
下面依然是一起看一下运行效果的对比。
 
 


四、最终的效果展示
 

还是那句话,贴几张场景的效果图和使用了屏幕特效后的效果图。在试玩场景时,除了类似CS/CF的FPS游戏控制系统以外,还可以使用键盘上的按键【F】,开启或者屏幕特效。
 
 
推车与货物(with 屏幕像素化特效):
 

推车与货物(原始场景):
 

城镇中(with 屏幕像素化特效):


 
城镇中(原始场景):

 
 
悠长小径(with 屏幕像素化特效):


 
悠长小径(原始场景):

 
 
山丘(with 屏幕像素化特效):

 

山丘(原始场景):

 
天色渐暗(with 屏幕像素化特效):

 
 
天色渐暗(原始场景):

 
 
云端(with 屏幕像素化特效):

 
云端原始场景):
 
 图就贴这些,更多画面大家可以从文章开头下载的本文配套的exe场景,进行试玩,或者在本文附录中贴出的下载链接中下载本文配套的所有游戏资源的工程。考虑到有读者朋友反映有时候打包出的unitypackage包会因为unity自身的bug不好打开。干脆从本期开始,我们以后项目工程就直接传项目的压缩包。大家解压出文件夹,然后直接用Unity打开即可。

至此,本文结束。感谢大家的捧场,我们下次更新再会。

PS:最近一段时间临近硕士毕业,有不少学业方面的事情需要处理,博客得停更一段时间,请见谅。





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

【百度云】博文示例场景exe下载
【百度云】博文示例场景资源和源码工程下载
【Github】屏幕像素化特效实现源码



2015-07-31 10:43:01 wolf96 阅读数 3563
这个位置可以看到ppsspp的特殊处理

文件位置

来看看这些特效

用来测试的未加特效图片

ppsspp:

传说系列一生爱---英杰传说


最后的战士



aacolor

是关于饱和度,亮度,对比度,色调的调节,
ppsspp中的默认参数为饱和度加强1.2倍,对比度增强1.25倍,在unity中我们可以设为外部变量自己调节

关键代码:

		float4 frag(v2f i) :COLOR
		{
			float size = 1 / _Size;

			float3 c10 = tex2D(_MainTex, i.uv_MainTex + float2(0, -1)*size).rgb;
			float3 c01 = tex2D(_MainTex, i.uv_MainTex + float2(-1, 0)*size).rgb;
			float3 c11 = tex2D(_MainTex, i.uv_MainTex).rgb;
			float3 c21 = tex2D(_MainTex, i.uv_MainTex + float2(1, 0)*size).rgb;
			float3 c12 = tex2D(_MainTex, i.uv_MainTex + float2(0, 1)*size).rgb;

			float3 dt = float3(1.0, 1.0, 1.0);
			float k1 = dot(abs(c01 - c21), dt);
			float k2 = dot(abs(c10 - c12), dt);

			float3 color = (k1*(c10 + c12) + k2*(c01 + c21) + 0.001*c11) / (2.0*(k1 + k2) + 0.001);

			float x = sqrt(dot(color, color));

			color.r = pow(color.r + 0.001, _A);
			color.g = pow(color.g + 0.001, _A);
			color.b = pow(color.b + 0.001, _A);

			//饱和度,亮度,对比度,色调映射
			return float4(contrast4(x)*normalize(color*_C_ch)*_B,1);

		}



ppsspp中实测


unity中实测



bloom

辉光效果
ppsspp中的辉光在unity种效果并不好,模糊处理不够,亮度过量,采样方式为grid采样
之前写过一篇详细博文关于HDR(与bloom相似)

关键代码:

	float4 frag(v2f i) :COLOR
			{
				float size = 1 / _Size;
				float2 uv = i.uv_MainTex;


				float3 color = tex2D(_MainTex, i.uv_MainTex);
				float4 sum = 0;
				float3 bloom;


				for (int i = -3; i < 3; i++)
				{
					sum += tex2D(_MainTex, uv + float2(-1, i)*size) * _Amount;
					sum += tex2D(_MainTex, uv + float2(0, i)*size) * _Amount;
					sum += tex2D(_MainTex, uv + float2(1, i)*size) * _Amount;
				}

				if (color.r < 0.3 && color.g < 0.3 && color.b < 0.3)
				{
					bloom = sum.rgb*sum.rgb*0.012 + color;
				}
				else
				{
					if (color.r < 0.5 && color.g < 0.5 && color.b < 0.5)
					{
						bloom = sum.xyz*sum.xyz*0.009 + color;
					}
					else
					{
						bloom = sum.xyz*sum.xyz*0.0075 + color;
					}
				}

				bloom = mix(color, bloom, _Power);

				return float4(bloom, 1);
			}



ppsspp中实测


unity中实测



cartoon

卡通效果的post processing
颜色总共分为四层,把颜色灰度每层段数值再加上减到最少的小数部分,产生一些过渡效果
再用得出的灰度乘上原色值,保留了原来的颜色只是明暗分为四层,层之间有过度
通过边缘检测描边,着色道理与之前写过的一篇详细博文像似,但不完全相同,

关键代码:

	float4 frag(v2f i) :COLOR
			{
				float size = 1 / _Size;

				float3 c00 = tex2D(_MainTex, i.uv_MainTex + float2(-1, -1)*size).rgb;
				float3 c10 = tex2D(_MainTex, i.uv_MainTex + float2(0, -1)*size).rgb;
				float3 c20 = tex2D(_MainTex, i.uv_MainTex + float2(1, -1)*size).rgb;
				float3 c01 = tex2D(_MainTex, i.uv_MainTex + float2(-1, 0)*size).rgb;
				float3 c11 = tex2D(_MainTex, i.uv_MainTex).rgb;
				float3 c21 = tex2D(_MainTex, i.uv_MainTex + float2(1, 0)*size).rgb;
				float3 c02 = tex2D(_MainTex, i.uv_MainTex + float2(-1, 1)*size).rgb;
				float3 c12 = tex2D(_MainTex, i.uv_MainTex + float2(0, 1)*size).rgb;
				float3 c22 = tex2D(_MainTex, i.uv_MainTex + float2(1, 1)*size).rgb;
				float3 dt = float3(1.0, 1.0, 1.0);

				float d1 = dot(abs(c00 - c22), dt);
				float d2 = dot(abs(c20 - c02), dt);
				float hl = dot(abs(c01 - c21), dt);
				float vl = dot(abs(c10 - c12), dt);
				float d = _Bb*(d1 + d2 + hl + vl) / (dot(c11, dt) + 0.15);
	
				float lc = 4.0*length(c11);
			
				float f = frac(lc); 
				f *= f;
				lc = 0.25*(floor(lc) + f*f) + 0.05;
				//颜色总共分为四层,把颜色灰度每层段数值再加上减到最少的小数部分,产生一些过渡效果
			
				c11 = 4.0*normalize(c11);
				float3 frct = frac(c11); 
				frct *= frct;
				c11 = floor(c11) + 0.05*dt + frct*frct;
				return float4(0.25*lc*(1.1 - d*sqrt(d))*c11,1);
				//再用得出的灰度乘上原色值,保留了原来的颜色只是明暗分为四层,层之间有过度
				//通过边缘检测描边,着色道理与之前的一篇文章像似,但不完全相同,

			}



ppsspp中实测


unity中实测



CRT

CRT是模拟以前大脑袋电脑的阴极射线管(Cathode Ray Tube)的显示器的特殊效果,频闪之类的特效,用过大脑袋的都懂。

关键代码:

		float4 frag(v2f i) :COLOR
			{

				// scanlines
				float vPos = float((i.uv_MainTex.y + _Time.z * 0.5) * 272.0);
				float j = 2;
				float line_intensity = modf(float(vPos), j);

				// color shift
				float off = line_intensity *0.00001;
				float2 shift = float2(off, 0);

				// shift R and G channels to simulate NTSC color bleed
				float2 colorShift = float2(0.001, 0);
				float r = tex2D(_MainTex, i.uv_MainTex + colorShift + shift).x;
				float g = tex2D(_MainTex, i.uv_MainTex - colorShift + shift).y;
				float b = tex2D(_MainTex, i.uv_MainTex).z;

				float4 c = float4(r, g * 0.99, b, 1) * clamp(line_intensity, 0.85, 1);

				float rollbar = sin((i.uv_MainTex.y + _Time.z) * 30);

				return c + (rollbar * 0.02);
			}



ppsspp中实测



unity中实测



fxaa

ppsspp的fxaa抗锯齿效果在unity上异常的好,耗费很少,
四个采样点来进行边缘检测(边缘检测的很粗糙),边缘处两个点之间模糊

关键代码:

	float4 frag(v2f i) :COLOR
			{

				float	u_texelDelta = 1 / _Size;



				float FXAA_SPAN_MAX = 8.0;
				float FXAA_REDUCE_MUL = 1.0 / 8.0;
				float FXAA_REDUCE_MIN = (1.0 / 128.0);

				float3 rgbNW = tex2D(_MainTex, i.uv_MainTex + (float2(-1.0, -1.0) * u_texelDelta)).xyz;
				float3 rgbNE = tex2D(_MainTex, i.uv_MainTex + (float2(+1.0, -1.0) * u_texelDelta)).xyz;
				float3 rgbSW = tex2D(_MainTex, i.uv_MainTex + (float2(-1.0, +1.0) * u_texelDelta)).xyz;
				float3 rgbSE = tex2D(_MainTex, i.uv_MainTex + (float2(+1.0, +1.0) * u_texelDelta)).xyz;
				float3 rgbM = tex2D(_MainTex, i.uv_MainTex).xyz;

				float3 luma = float3(0.299, 0.587, 0.114);
				float lumaNW = dot(rgbNW, luma);
				float lumaNE = dot(rgbNE, luma);
				float lumaSW = dot(rgbSW, luma);
				float lumaSE = dot(rgbSE, luma);
				float lumaM = dot(rgbM, luma);

				float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
				float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));

				float2 dir;
				dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
				dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));

				float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);
				
				float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
			
				dir = min(float2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),
					max(float2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), dir * rcpDirMin)) * u_texelDelta;
		
				float3 rgbA = (1.0 / 2.0) * (
					tex2D(_MainTex, i.uv_MainTex + dir * (1.0 / 3.0 - 0.5)).xyz +
					tex2D(_MainTex, i.uv_MainTex + dir * (2.0 / 3.0 - 0.5)).xyz);
				float3 rgbB = rgbA * (1.0 / 2.0) + (1.0 / 4.0) * (
					tex2D(_MainTex, i.uv_MainTex + dir * (0.0 / 3.0 - 0.5)).xyz +
					tex2D(_MainTex, i.uv_MainTex + dir * (3.0 / 3.0 - 0.5)).xyz);

				float lumaB = dot(rgbB, luma);

				if ((lumaB < lumaMin) || (lumaB > lumaMax)){
					return float4( rgbA,1);
				}
				else {
					return float4(rgbB, 1);
				}
				//整体上是一个边缘检测,在边缘处进行采样模糊
	
			}



ppsspp中实测


unity中实测



grayscale

灰度
白光的亮度用Y表示,它和红绿蓝三色光的关系:
Y = 0.299R+0.587G+0.114B    NTSC美制电视制式亮度公式
Y = 0.222R+0.707G+0.071B    PAL(相位逐行交变)电视制式
ppsspp使用的是NTSC美制,Unity中的Luminance函数使用的是PAL制式

关键代码:

float4 frag(v2f i) :COLOR
			{

				float3 rgb = tex2D(_MainTex, i.uv_MainTex).rgb;
				float luma = dot(rgb, float3(0.299, 0.587, 0.114));

				return luma;

			}

ppsspp中实测


unity中实测




inversecolors

反色

关键代码:

		float4 frag(v2f i) :COLOR
			{

				float3 rgb = tex2D(_MainTex, i.uv_MainTex).rgb;
				float luma = dot(rgb, float3(0.299, 0.587, 0.114));
			//	luma = Luminance(rgb);
				float3 gray = float3(luma, luma, luma) - 0.5;
				rgb -= float3(0.5, 0.5, 0.5);

				return float4(mix(rgb, gray, 2.0) + 0.5, 1);

			}



ppsspp中实测



unity中实测



natural

使颜色变得自然
把颜色从RGB转到YIQ色彩空间在进行变换

关键代码:

	float4 frag(v2f i) :COLOR
			{
				float3 val00 = float3(1.2, 1.2, 1.2);
				float3x3 RGBtoYIQ = float3x3(0.299, 0.596, 0.212,
				0.587, -0.275, -0.523,
				0.114, -0.321, 0.311);

				float3x3 YIQtoRGB = float3x3(1.0, 1.0, 1.0,
					0.95568806036115671171, -0.27158179694405859326, -1.1081773266826619523,
					0.61985809445637075388, -0.64687381613840131330, 1.7050645599191817149);

				float4 c = tex2D(_MainTex, i.uv_MainTex);
				float3 c1 =mul( RGBtoYIQ,c.rgb);

				c1 = float3(pow(c1.x, val00.x), c1.yz*val00.yz);
				//转换到YIQ色彩空间再加强GB颜色1.2倍
				return float4(mul(YIQtoRGB,c1), 1);
			}



ppsspp中实测



unity中实测



scanlines

屏幕上的线的效果

关键代码:

	float4 frag(v2f i) :COLOR
			{

				float pos0 = ((i.uv_MainTex.y + 1.0) * 170.0*_Amount);
				float pos1 = cos((frac(pos0) - 0.5)*3.1415926*_Inten)*1.5;
				float4 rgb = tex2D(_MainTex, i.uv_MainTex);

				// slight contrast curve
				float4 color = rgb*0.5 + 0.5*rgb*rgb*1.2;

				// color tint
				color *= float4(0.9, 1.0, 0.7, 0.0);

				// vignette
				color *= 1.1 - 0.6 * (dot(i.uv_MainTex - 0.5, i.uv_MainTex - 0.5) * 2.0);

				return mix(float4(0, 0, 0, 0), color, pos1);
			}



ppsspp中实测



unity中实测



sharpen

锐化
取上下两个采样点,采样点之间的颜色差越大(边缘,色差大等处),sharp越明显

关键代码:

		float4 frag(v2f i) :COLOR
			{
				float4 c = tex2D(_MainTex, i.uv_MainTex);
				c -= tex2D(_MainTex, i.uv_MainTex + _Size)*7.0*_Inten;
				c += tex2D(_MainTex, i.uv_MainTex - _Size)*7.0*_Inten;
				//采样点之间的颜色差越大(边缘,色差大等处),sharp越明显
				return c;
			}



ppsspp中实测



unity中实测



vignette

晕影效果

关键代码:

			float4 frag(v2f i) :COLOR
			{

				float vignette = 1.1 - 0.6 * (dot(i.uv_MainTex - 0.5, i.uv_MainTex - 0.5) * 2.0);
				float3 rgb = tex2D(_MainTex, i.uv_MainTex).rgb;
				return float4(vignette * rgb, 1);

			}



ppsspp中实测



unity中实测



4xhqglsl

平滑效果

ppsspp中实测


upscale_spline36

缩放滤镜
基于样条线的缩放(Spline based resizers)分别有spline16 spline36 spline64这三个缩放滤镜基于样条插值算法。样条差值算法在放大时尽可能的锐化图像,减少失真
该算法原作者为Panorama tools的开发者
详细算法的介绍请看forum.doom9.org/showthread.php?t=147117

ppsspp中实测

用一张不同的图做测试,在比例2X的情况下全屏拉伸,图像会变模糊,使用缩放滤镜进行放大,效果就像4X一样清楚

未开启




开启




全部代码已上传至GitHub


                                    --------by wolf96  http://blog.csdn.net/wolf96


2016-11-01 12:30:37 kevlis 阅读数 1981

简述

本文将通过一个简单的例子来简述Unity3D中的Image Effects屏幕后期处理特效的过程。

根据前面的翻译可以知道,Unity3D的屏幕特效可以概括为:

Created with Raphaël 2.1.0获取当前整个屏幕的画面,作为Source Image(SI),即来源图像。建立材质,用于处理添加Shader处理SI。使用Shader对Source Image进行处理。使用Graphics.Blit将处理结果递交给Unity3D渲染器加以应用

下面通过一个简单的ImageEffect实例来进行了解

脚本

using UnityEngine;
using System.Collections;

[ExecuteInEditMode] // 使脚本可以直接在编辑模式下运行,而不用运行游戏,方便预览
public class CleanRChannel : MonoBehaviour
{
    public Shader curScreenShader;  // 用来处理屏幕效果的Shader
    Material curScreenMat;  // 用来处理屏幕效果的材质

    Material screenMat
    {
        get
        {
            if (curScreenMat == null)
                curScreenMat = new Material(curScreenShader);
            return curScreenMat;
        }
    }
    // 检查目标平台是否支持ImageEffect和相应的Shader,不支持的话就禁用脚本
    // 检查当前Shader是否存在,并且是否在目标平台上可以使用
    void CheckPlatformSupport()
    {
        if (!SystemInfo.supportsImageEffects)
        {
            enabled = false;
            return;
        }

        if (!curScreenShader || !curScreenShader.isSupported)
            enabled = false;
    }

    // 投递后处理效果的代码必须置于OnRenderImage中
    // src 来源RenderTexture,实际上就是当前屏幕的画面
    void OnRenderImage(RenderTexture src, RenderTexture des)
    {
        if (curScreenShader)
            Graphics.Blit(src, des, screenMat);
        else
            Graphics.Blit(src, des);
    }

    void OnDisable()
    {
        if (curScreenMat)
            DestroyImmediate(curScreenMat);
    }

    // 游戏开始时先检测相关内容是否支持,不支持就直接禁用
    void Start()
    {
        CheckPlatformSupport();
    }
}

着色器

着色器即Shader,是用来实现图像渲染的用来替代固定渲染管线的可编程工具。
今后的文章中可能会对其进行详解。目前暂时会在代码中对些进行一些注释。
以下着色器使用的是可编程着色器,具有顶点着色器Vertex和片段着色器Frag两个函数,ImageEffects使用的着色器皆为可编程着色器。

// 去除红色通道Shader
// Shader的名称,Unity3D中Hidden/开头的Shader会在材质Inspector中被隐藏,由于我们只用于ImageEffect,所以将其放在Hidden下
Shader "Hidden/CleanRChannel"       
{
    // 属性,包含有一个_MainTex,即将要被用来作处理的纹理,在U3D中,ImageEffects中的_MainTex自动被视为当前屏幕图像,因此_MainTex即Source Image
    Properties 
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        // 前面翻译的官方文档中有说过,许多时候ImageEffect中不需要以下信息,本例中也不需要,因此将其关闭
        Cull Off ZWrite Off ZTest Always

        // Pass,一个Pass即代表一次处理。本例操作只需要一次处理即可完成
        Pass
        {
            CGPROGRAM
            // #pragma vertex + [vertex functionName] 顶点着色器函数名
            #pragma vertex vert
            // #pragma fragment + [fragmentfunctionName] 片段(像素)着色器函数名
            #pragma fragment frag    
            // 引用的U3D自己的Shader相关的头文件,里面有一些常用函数或参数
            #include "UnityCG.cginc"  

            // 顶点函数的输入结构体,变量寄存在POSITION和TEXCOORD0中。POSITION和TEXCOORD0称为语义绑定(semantics binding),留待以后讲解。
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            // 片段函数的输入结构体,也是顶点函数的输出。SV_系列是DX10中新增的语义绑定。
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                // 通过MVP矩阵变换顶点。相关数学知识可查阅《3D Math Primer for Graphics and Game Development》或大学线性代数
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                // 根据UV坐标取得颜色值
                fixed4 col = tex2D(_MainTex, i.uv);
                // 去除红色通道
                col.r = 0;       
                return col;
            }
            ENDCG
        }
    }
}

效果执行前后对比

原始画面:
执行前

去除红色通道:
执行后

2015-05-01 16:22:33 baidu_26153715 阅读数 9417

Unity shader(CG) 写一个 散色、折射、反射、菲涅尔、gamma、简单后期屏幕特效

1.自生要求是很重要的,当然不是什么强迫工作之类的,而是自己有限的能力上不断的扩展兴趣上的内容。
2.用生活的眼光去发现shader的存在形式,许多灵感都来自于大自然,比如 火苗的动态抖动 像 frac/fract(time)函数,甚至小草被风吹也像frac/fract(time).这里写图片描述http://www.glslsandbox.com/e#24095.1
3.千万不要有压力,而是动力,每次调颜色和参数都有不经意的收获,多次尝试是产生意外之一,感觉会油然而生,当然要抓住它它它它它它它它。。。


转载请出处地址 http://blog.csdn.net/baidu_26153715/article/details/45420029 恬纳微晰


如果写效果头疼,那么

  1. 1.这这这,不就是透射的感觉么?

    这里写图片描述

  2. 折射率改变

    这里写图片描述

  3. 散色率的改变
    这里写图片描述

  4. 透射颜色改变
    这里写图片描述

    如果看效果眼睛疼,那么

  5. 后期屏幕特效-HDR之亮度映射效果
    这里写图片描述

  6. 后期屏幕特效-HDR之色调映射效果
    这里写图片描述

  7. 后期屏幕特效-模拟gamma效果
    这里写图片描述

  8. 什马效果
    当然除了以上效果,还有bloom,glow,streak,ghost,blur,flare,次表面散色等等效果,你直接wiki和Nvida官网上得到相关资料。当然搭配得好才是,,,不然耗了硬件又耗了审美观。。

慢慢来,不要强求一下子完美

  • 反射
    这里写图片描述
    物理上我们常常学到的是环境中的光线入射到介质里,产生了折射变化。而对于一个unity游戏引擎来说光是非常宝贵的,unity5.x中的GI全局光照什马的采用so easy版光线追踪,什马的ray_cast、ray_track、ray_march等等,我们常用到的还是ray-cast…….

于是我们可以用视线投射出光线,逆着光路查询颜色。

这里写图片描述
当然至于查询颜色,像这种360度无死角反射,让你会毫不犹豫的想到它(?????)

这里写图片描述
对!!!就是它了,cubemap,当然你也要先samplerCUBE采样器采样。

  • 那散色与折射呢?
    这里写图片描述
    亦夫如是,不过要下点功夫而已,什么是散射,它和折射到底是父子关系还是夫妻关系?(这个嘛!属于物理,可以wiki百科啊、、)

    那怎么用shader写出散色,如果知道rgb通道,那么做做手脚也是不错的选择。(三原色,哈哈哈!!!)
    这里写图片描述

  • fresnel(菲涅尔)好高大上的样子
    这里写图片描述
    OK!!!就是这么着,这是我简版菲涅尔。
    其实它就是我们可以认为的边缘光照(这里写图片描述),有可以用它做为描边效果,而我用它做做差值反射与折射效果,这个pow()嘛!你懂得!

用用后期效果试试

  • 我实在是不行了,快来救救我,于是
    这里写图片描述
    对,没有猜错,这就是so easy版camera与shader打交道的方式之一

    这里写图片描述
    这是传递camera图像的,a图片传给shader(gpu),b图片就是shader渲染完的图片给camera显示啊。你当然可以看看 unity 自带的 API说明就是了。

  • 后期shader

    这里写图片描述就是Ta,一个又是自己写的shader,,,其实自己写shader才放心,还能变花样,还能优化!当然也有人怀疑自己弄得太神,哈哈哈哈哈哈,天朝gpu编程有多大的历史啊,有好多靠wiki学习,再说这么点代码,太美妙了。

  • 亮度(Luminance)

    用用Luminance是个不错的选择

    这里写图片描述
    对亮度合理的分配。

  • 色调映射(toneMapping)
    http://en.wikipedia.org/wiki/Tone_mapping这是Tonemapping的wiki解释
    这里写图片描述

    对!就它了,图中圈出的so easy版tonemapping 过滤器。。。。
    这里写图片描述
    当然,还有更好的tonemapping过滤器,你可以自己去发明或看看理论。

  • gamma
    一个so easy gamma版本
    这里写图片描述

  • 更叼的算法和特效,你说呢?
    那是不会放出来的,有的埋在土里,有的就在wiki中,作为一个博客,为了不让别人把重要的东西复制走了,你除了水印就是代码替换,保护自己的著作版权。。。

    视觉一览通

    这里写图片描述这里写图片描述这里写图片描述

    我认为雕像可以的样子

    背景-全景图cubemap来自wiki百科下载,特效渲染归 恬纳微晰所有

    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

  • 有空把 曲面细分 拿来说说,把 sun shader拿来说说,还有好多多…………
没有更多推荐了,返回首页