开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:1871

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 2766|回复: 24

[浅墨] 【浅墨Unity3D Shader编程】之十一 深入理解Unity5中的Standard Shader(三)&屏幕像素化特效的实现

[复制链接]  [移动端链接]
抢楼 抢楼 本帖为抢楼帖,欢迎抢楼! 
排名
6131
昨日变化
3

31

主题

51

帖子

1754

积分

Rank: 9Rank: 9Rank: 9

UID
60428
好友
31
蛮牛币
1601
威望
0
注册时间
2014-12-10
在线时间
74 小时
最后登录
2017-7-10

原创先锋认证开发者

发表于 2015-11-30 10:05:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 浅墨 于 2015-11-30 12:57 编辑

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

概要:续接上文,本文进一步讲解与分析了上文未讲完的中Standard Shader正向基础渲染通道源码的片段着色实现部分,以及对屏幕像素化后期特效进行了实现。

同样需要声明的是。本文中对Stardard Shader源码的一些分析,全是浅墨自己通过对Unity Shader内建源码的理解,以及Google之后理解与分析而来。如有解释不妥当之处,还请各位及时指出。

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

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

1.jpg

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

2.jpg

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

【可运行的本文配套exe游戏场景请点击这里下载】
http://pan.baidu.com/s/1dDBcMWT


提示:在此游戏场景中按F键可以开关屏幕特效。

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

本次源码剖析有涉及BRDF的相关内容,这边简单提一下。
双向反射分布函数(Bidirectional ReflectanceDistribution Function,BRDF)用来定义给定入射方向上的辐射照度(irradiance)如何影响给定出射方向上的辐射率(radiance)。更笼统地说,它描述了入射光线经过某个表面反射后如何在各个出射方向上分布——这可以是从理想镜面反射到漫反射、各向同性(isotropic)或者各向异性(anisotropic)的各种反射。

BRDF作为图形学中比较常见的一个知识点,这边暂时不多讲,因为随便拿一本图形学相关的书都可以看到他的身影。这边给出一些参考的链接,大家有需要可以深入了解:

1. 如何正确理解 BRDF (双向反射分布函数)? - 计算机 - 知乎
http://www.zhihu.com/question/20286038

2.图形学理论知识:BRDF 双向反射分布函数
http://www.cnblogs.com/mengdd/archive/2013/08/05/3237991.html

3. An Introduction to BRDF-based Lighting –Nvidia
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=10&ved=0ahUKEwjakrqgz63JAhXJm5QKHZvQAjkQFghRMAk&url=http%3A%2F%2Fwww.nvidia.com%2Fattach%2F6568&usg=AFQjCNFiU3qO0AG2__Rv1SGTN4GmXLIXeg

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

此部分接上文《【浅墨Unity3D Shader编程】之十 深入理解Unity5中的Standard Shader(二)&屏幕油画特效的实现》的第二部分“Standard Shader中正向基础渲染通道源码分析“。
上文中分析了Standard Shader中正向基础渲染通道的源码,刚好分析完了顶点着色函数vertForwardBase,本文中将对片段着色函数fragForwardBase 进行说明。分析完之后,也就结束了这一系列长得稍微有些离谱的Standard Shader正向基础渲染通道的源码分析。

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

[C#] 纯文本查看 复制代码
//----------------------------------------【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变量。

[C#] 纯文本查看 复制代码
#define FRAGMENT_SETUP(x) FragmentCommonData x = \  
    FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));  


调用此宏,也就是表示写了如下的代码,定义了一个x变量:

[C#] 纯文本查看 复制代码
FragmentCommonData x =FragmentSetup(i.tex, i.eyeVec, IN_VIEWDIR4PARALLAX(i), i.tangentToWorldAndParallax, IN_WORLDPOS(i));  


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

[C#] 纯文本查看 复制代码
//函数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头文件中:

[C#] 纯文本查看 复制代码
//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结构体对象,并进行相应的填充,其返回值作为主光源。相关代码如下:

[C#] 纯文本查看 复制代码
//  用途:该函数为主光照函数  
//  说明:实例化一个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头文件中,用于实现阴影渲染相关的辅助工作,代码如下:

[C#] 纯文本查看 复制代码
// ----------------  
//  阴影相关工具代码 || 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函数中。实现代码如下。

[C#] 纯文本查看 复制代码
//------------------------------【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函数的原型如下:

[C#] 纯文本查看 复制代码
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,具体定义如下:

[C#] 纯文本查看 复制代码
half LerpOneTo(half b, half t)  
{  
    half oneMinusT = 1 - t;  
    return oneMinusT + b * t;  
}  


5. UnityGI结构体

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

[C#] 纯文本查看 复制代码
//全局光照结构体  
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头文件中,代码如下。

[C#] 纯文本查看 复制代码
//Unity中光源参数的结构体  
struct UnityLight  
{  
    half3 color;//光源颜色  
    half3 dir;//光源方向  
    half  ndotl; //入射光方向和当前表面法线方向的点积  
};  
  
//Unity中间接光源参数的结构体  
struct UnityIndirect  
{  
    half3 diffuse;//漫反射颜色  
    half3 specular;//镜面反射颜色  
};  


6. FragmentGI函数

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

[C#] 纯文本查看 复制代码
//函数:片段着色部分全局光照的处理函数  
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结构体定义了全局光照所需要的一些函数,定义为如下:

[C#] 纯文本查看 复制代码
//全局光照的输入参数结构体  
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头文件中,实现如下。

[C#] 纯文本查看 复制代码
inline UnityGI UnityGlobalIllumination (UnityGIInput data, half occlusion, half3 normalWorld)  
{  
    return UnityGI_Base(data, occlusion, normalWorld);  
}  


此FragmentGI函数就是嵌套了一层UnityGI_Base函数,那我们继续溯源,找到UnityGI_Base函数的定义,也是位于UnityGlobalIllumination.cginc头文件中:

[C#] 纯文本查看 复制代码
//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。

[C#] 纯文本查看 复制代码
//-------------------------------------------------------------------------------------  
// 默认使用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。上文有贴出过其实现代码,都几次提到了,这边就再贴一下。

[C#] 纯文本查看 复制代码
struct UnityLight  
{  
    half3 color;//光源颜色  
    half3 dir;//光源方向  
    half  ndotl; //入射光方向和当前表面法线方向的点积  
};  


第八个参数,UnityIndirect类型的gi ,一个包含了half3型的漫反射颜色diffuse和half3型的镜面反射颜色specular的光线反射结构体,表示间接光照信息。

[C#] 纯文本查看 复制代码
struct UnityIndirect  
{  
    half3 diffuse;//漫反射颜色  
    half3 specular;//镜面反射颜色  
};  


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

7.1 BRDF1_Unity_PBS

[C#] 纯文本查看 复制代码
//最高特效的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

[C#] 纯文本查看 复制代码
// 为移动平台简化的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

[C#] 纯文本查看 复制代码
// 为小于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头文件中,相关代码如下。

[C#] 纯文本查看 复制代码
//-------------------------------------------------------------------------------------  
// 从间接的方向光照贴图中进行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是一个函数名,就紧紧跟在上面这段宏代码的后面:

[C#] 纯文本查看 复制代码
//间接光照的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函数,对输入的纹理进行光照贴图的采样,相关代码如下:

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


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

[C#] 纯文本查看 复制代码
sampler2D   _EmissionMap;  


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

[C#] 纯文本查看 复制代码
//自发光纹理图  
_EmissionMap("Emission", 2D) = "white" {}  


10.UNITY_APPLY_FOG宏

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

[C#] 纯文本查看 复制代码
//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头文件中,其为正向渲染通道的输出函数。

[C#] 纯文本查看 复制代码
//-----------------------------【函数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宏的定义为:

[C#] 纯文本查看 复制代码
#define UNITY_OPAQUE_ALPHA(outputAlpha) outputAlpha = 1.0  


三、屏幕像素化特效的实现
  
我们都知道,Unity中的屏幕特效通常分为两部分来实现:
Shader实现部分
脚本实现部分
下面依旧是从这两个方面对本次的特效进行实现。

3.1 Shader实现部分
  
国际惯例,上注释好的Shader代码。

[C#] 纯文本查看 复制代码
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代码中所展示的,本次的屏幕像素化特效主要用一个自定义函数来实现,实现代码如下:

[C#] 纯文本查看 复制代码
//进行像素化操作的自定义函数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的返回值即可:

[C#] 纯文本查看 复制代码
fixed4 frag(vertexOutput  Input) : COLOR  
    {  
        //使用自定义的PixelateOperation函数,计算每个像素经过取整后的颜色值  
        return PixelateOperation(_MainTex, Input.uv, _Params.x, _Params.y);  
    }  


3.2 C#脚本实现部分

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参数自己定制像素的长宽比。

1.png

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

四、最终的效果展示

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

3.jpg

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

4.jpg

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

5.jpg

城镇中(原始场景):

6.jpg

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

7.jpg

悠长小径(原始场景):

8.jpg

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

9.jpg

山丘(原始场景):

10.jpg

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

11.jpg

天色渐暗(原始场景):

12.jpg

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

13.jpg

云端(原始场景):

14.jpg

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

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

附: 本博文相关下载链接清单
游客,如果您要查看本帖隐藏内容请回复





unity3dunity5;unity5 standard;unity3d 浅墨;unity5激活码;unity5.0雾效;unity5.0 material;unity5.3 雾;unity3d fps 游戏源码;unity5.0标准资源包;浅墨unity shader之三;unity烘焙 含义;unity5.0烘焙参数;unity5.0 lightmap;unity5.1激活;unity standard shader source;unity5.0 打包场景;unity5 fog在哪;unity;unity5 unity3d;unity5.0 standard;浅墨unity3d;unity5.3 激活码;unity5.0激活码;unity5.0.2激活码;unity5雾效;unity5.3雾效;unity5 material;unity5雾;unity5雾看不到;unity3d的fps游戏源码;unity5.3标准资源包;unity5标准资源包;unity5.1标准资源包;浅墨 unity shader;unity shader 浅墨;unity烘焙的含义;unity5 烘焙参数;unity5.3 lightmap;unity5.2 lightmap;unity5.2激活;unity5激活;unity5.0激活;unity5.2.3激活;unity 5 standard shader source;unity5 打包场景;unity5.3 场景打包;unity5.0 fog;浅墨 unity

回复

使用道具 举报

5熟悉之中
871/1000
排名
1916
昨日变化
3

1

主题

135

帖子

871

积分

Rank: 5Rank: 5

UID
52895
好友
0
蛮牛币
1513
威望
0
注册时间
2014-11-3
在线时间
175 小时
最后登录
2017-8-14
发表于 2015-11-30 20:57:32 | 显示全部楼层
支持一下楼主!!感谢分享这么好的教程!!

回复

使用道具 举报

3偶尔光临
157/300
排名
14619
昨日变化
11

0

主题

32

帖子

157

积分

Rank: 3Rank: 3Rank: 3

UID
80706
好友
0
蛮牛币
261
威望
0
注册时间
2015-3-17
在线时间
92 小时
最后登录
2017-9-12
发表于 2015-12-1 09:52:28 | 显示全部楼层
学习学习

回复

使用道具 举报

头像被屏蔽
排名
1127
昨日变化
2

11

主题

454

帖子

1998

积分

UID
43291
好友
5
蛮牛币
4602
威望
0
注册时间
2014-9-2
在线时间
654 小时
最后登录
2016-7-8

活力之星

发表于 2015-12-1 11:13:45 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽

回复

使用道具 举报

7日久生情
2164/5000
排名
451
昨日变化
2

4

主题

291

帖子

2164

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
50936
好友
0
蛮牛币
6310
威望
0
注册时间
2014-10-24
在线时间
611 小时
最后登录
2017-9-23
发表于 2015-12-1 21:07:44 | 显示全部楼层
深入理解Unity5中的Standard Shader

回复

使用道具 举报

7日久生情
2671/5000
排名
107
昨日变化

0

主题

126

帖子

2671

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
6304
好友
0
蛮牛币
6317
威望
0
注册时间
2013-10-24
在线时间
577 小时
最后登录
2017-9-23
发表于 2015-12-2 09:35:52 | 显示全部楼层
好文.....

回复

使用道具 举报

4四处流浪
426/500
排名
6237
昨日变化
2

0

主题

23

帖子

426

积分

Rank: 4

UID
101862
好友
0
蛮牛币
188
威望
0
注册时间
2015-5-20
在线时间
245 小时
最后登录
2017-8-3
发表于 2015-12-2 10:30:21 | 显示全部楼层
shader的应用确实不是很明白,感谢楼主

回复

使用道具 举报

7日久生情
2184/5000
排名
615
昨日变化
2

1

主题

636

帖子

2184

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
19501
好友
0
蛮牛币
4372
威望
0
注册时间
2014-3-29
在线时间
448 小时
最后登录
2017-6-17
发表于 2015-12-2 17:49:32 | 显示全部楼层
【浅墨Unity3D Shader编程】之十一 深入理解Unity5中的Standard Shader(三)&屏幕像素化特效的实现

回复

使用道具 举报

5熟悉之中
717/1000
排名
3131
昨日变化
2

0

主题

109

帖子

717

积分

Rank: 5Rank: 5

UID
4258
好友
1
蛮牛币
1851
威望
0
注册时间
2013-9-21
在线时间
249 小时
最后登录
2017-8-23

VIP

发表于 2015-12-2 19:50:57 | 显示全部楼层
支持LZ分享

回复

使用道具 举报

7日久生情
3279/5000
排名
212
昨日变化

0

主题

214

帖子

3279

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
89115
好友
1
蛮牛币
7583
威望
0
注册时间
2015-4-3
在线时间
1423 小时
最后登录
2017-9-23
发表于 2015-12-3 09:20:53 | 显示全部楼层
好资料,值得收藏。

回复

使用道具 举报

7日久生情
2502/5000
排名
310
昨日变化
1

5

主题

434

帖子

2502

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
32498
好友
3
蛮牛币
4159
威望
0
注册时间
2014-7-4
在线时间
609 小时
最后登录
2017-9-22
发表于 2015-12-3 09:41:23 | 显示全部楼层

支持一下楼主!!感谢分享这么好的教程!!

回复

使用道具 举报

7日久生情
2064/5000
排名
676
昨日变化
1

1

主题

316

帖子

2064

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
29810
好友
3
蛮牛币
4865
威望
0
注册时间
2014-6-16
在线时间
684 小时
最后登录
2017-9-22
发表于 2015-12-3 09:49:34 | 显示全部楼层
支持一下楼主!!感谢分享这么好的教程!!
[发帖际遇]: 没问题 被钱袋砸中进医院,看病花了 2 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

7日久生情
1820/5000
排名
939
昨日变化
3

3

主题

285

帖子

1820

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
98468
好友
1
蛮牛币
845
威望
0
注册时间
2015-5-7
在线时间
642 小时
最后登录
2017-9-23
QQ
发表于 2015-12-3 10:14:10 | 显示全部楼层
支持一下楼主!!感谢分享这么好的教程!!

回复

使用道具 举报

8常驻蛮牛
5434/10000
排名
252
昨日变化

0

主题

2634

帖子

5434

积分

Rank: 8Rank: 8

UID
3215
好友
0
蛮牛币
264
威望
0
注册时间
2013-9-4
在线时间
1240 小时
最后登录
2017-9-22
发表于 2015-12-3 17:53:04 | 显示全部楼层
谢谢楼主,分享好东西啊啊啊啊啊

回复

使用道具 举报

6蛮牛粉丝
1461/1500
排名
855
昨日变化

0

主题

190

帖子

1461

积分

Rank: 6Rank: 6Rank: 6

UID
25261
好友
0
蛮牛币
1621
威望
0
注册时间
2014-5-15
在线时间
329 小时
最后登录
2016-12-13
发表于 2015-12-15 17:18:14 | 显示全部楼层
感谢分享!!!!!!

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表