小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

【淺墨Unity3D Shader編程】之十 深入理解Unity5中的Standard Shader...

 釋放_入微_成長 2016-09-22

 


本系列文章由@淺墨_毛星云 出品,轉(zhuǎn)載請注明出處。  

文章鏈接: http://blog.csdn.net/poem_qianmo/article/details/49719247

作者:毛星云(淺墨)    微博:http://weibo.com/u/1723155442

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

 

概要:本文講解了Unity中著色器編譯多樣化的思路,并對Standard Shader中正向基礎渲染通道的源碼進行了分析,以及對屏幕油畫特效進行了實現(xiàn)。

 

眾所周知,Unity官方文檔對Shader進階內(nèi)容的講解是非常匱乏的。本文中對Stardard Shader源碼的一些分析,全是淺墨自己通過對Shader源碼的理解,以及Google之后理解與分析而來。如有解釋不妥當之處,還請各位及時指出。

 

依然是附上一組本文配套工程的運行截圖之后,便開始我們的正文。本次的選用了新的場景,正如下圖中所展示的。

城鎮(zhèn)入口(with 屏幕油畫特效):



城鎮(zhèn)入口(原始圖):



 圖依然是貼這兩張。文章末尾有更多的運行截圖,并提供了源工程的下載。先放出可運行的exe下載,如下:


【可運行的本文配套exe游戲場景請點擊這里下載】

 

提示:在此游戲場景中按F鍵可以開關屏幕特效。

著色器編譯多樣化算是Unity5中Shder書寫的新特性,標準著色器之所以能獨當一面,正是得益于這種特性,在這里先對此特性進行一個簡單的說明與講解。

 

 




一、關于著色器編譯多樣化


 

此部分參考自Unity5.2.1版官方文檔(http://docs./Manual/SL-MultipleProgramVariants.html),經(jīng)翻譯&理解后而成。如有解釋不妥當之處,還請各位及時指出。

Unity5中使用了一種被稱為著色器編譯多樣化(Multiple shader program variants)的新技術,常被稱為“megashaders”或“uber shaders”,并通過為每種情況提供不同的預處理指令來讓著色器代碼多次被編譯來實現(xiàn)。

在Unity中,這可以通過#pragmamulti_compile或者#pragma shader_feature指令來在著色器代碼段中實現(xiàn)。這種做法對表面著色器也可行。

在運行時,相應的著色器變體是從材質(zhì)的關鍵詞中取得的(Material.EnableKeyword和 DisableKeyword),或者全局著色器關鍵詞(Shader.EnableKeyword和 DisableKeyword)。



1.1 multi_compile的用法簡析


若我們定義如下指令:

#pragma multi_compile FANCY_STUFF_OFFFANCY_STUFF_ON

也就表示定義了兩個變體:FANCY_STUFF_OFF和FANCY_STUFF_ON。在運行時,其中的一個將被激活,根據(jù)材質(zhì)或者全局著色器關鍵詞(#ifdef FANCY_STUFF_OFF之類的宏命令也可以)來確定激活哪個。若兩個關鍵詞都沒有啟用,那么將默認使用前一個選項,也就是關閉(OFF)的選項FANCY_STUFF_OFF。

需要注意,也可以存在超過兩個關鍵字的multi_compile編譯選項,比如,如下代碼將產(chǎn)生4種著色器的變體:

#pragma multi_compile SIMPLE_SHADINGBETTER_SHADING GOOD_SHADING BEST_SHADING

當#pragma multi_compile中存在所有名字都是下劃線的一個指定段時,就表示需在沒有預處理宏的情況下產(chǎn)生一個空的著色器變種。這種做法在著色器編寫中比較常見,因為這樣可以在不影響使用的情況下,避免使用兩個關鍵詞,這樣就節(jié)省了一個變量個數(shù)的占用(下面會提到,Unity中關鍵詞個數(shù)是有129個的數(shù)量限制的)。例如,下面的指令將產(chǎn)生兩個著色器變體;第一個沒有定義,第二個定義為FOO_ON:

#pragma multi_compile __ FOO_ON

這樣就省去了一個本來需要定義出來的 FOO_OFF(FOO_OFF沒有定義,自然也不能使用),節(jié)省了一個關鍵詞個數(shù)的占用。

若Shader中有如上定義,則可以使用#ifdef來進行判斷:

#ifdef FOO_ON//代碼段1#endif

根據(jù)上面已經(jīng)定義過的FOO_ON,此#ifdef判斷的結果為真,代碼段1部分的代碼就會被執(zhí)行到。反之,若#pragma multi_compile __FOO_ON一句代碼沒有交代出來,那么代碼段1部分的代碼就不會被執(zhí)行。

這就是著色器編譯多樣化的實現(xiàn)方式,其實理解起來很容易,對吧。

 


1.2 shader_feature和multi_compile之間的區(qū)別

 

#pragma shader_feature 和#pragma multi_compile非常相似,唯一的區(qū)別在于采用了#pragmashader_feature語義的shader,在遇到不被使用的變體的時候,就不會將其編譯到游戲中。所以,shader_feature中使得所有的設置到材質(zhì)中的關鍵詞都是有效的,而multi_compile指令將從全局代碼里設置關鍵詞。

另外,shader_feature還有一個僅僅含有一個關鍵字的快捷表達方式,例如:

#pragma shader_feature FANCY_STUFF


此為#pragma shader_feature _ FANCY_STUFF的一個簡寫形式,其擴展出了兩個著色器變體,第一種變體自然為不定此FANCY_STUFF變量(那么若在稍后的Shader代碼中進行#ifdef FANCY_STUFF的判斷,則結果為假),第二種變體為定義此FANCY_STUFF變量(此情況下#ifdef FANCY_STUFF的判斷結果為真)。



1.3 多個multi_compile連用會造成指數(shù)型增長

 

可以提供多個multi_compile流水線,然后著色器的結果可以被編譯為幾個流水線的排列組合,比如:

#pragma multi_compile A B C#pragma multi_compile D E

第一行中有3種選項,第二行中有兩種選項,那么進行排列組合,總共就會有六種選項(A+D, B+D, C+D, A+E, B+E, C+E)。

容易想到,一般每以個multi_compile流水線,都控制著著色器中某一單一的特性。請注意,著色器總量的增長速度是非??斓?。

比如,10條包含兩個特性的multi_compil指令,會得到2的10次方,也就是1024種不同的著色器變體。

 


1.4 關于Unity中的關鍵詞限制Keyword limit

 

當使用著色變量時,我們應該記住,Unity中將關鍵詞的數(shù)量限制在了128個之內(nèi)(著色變量算作關鍵字),且其中有一些已經(jīng)被Unity內(nèi)置使用了,因此,我們真正可以自定義使用關鍵詞的數(shù)量以及是小于128個的。同時,關鍵詞是在單個Unity項目中全局使用并計數(shù)的,所以我們要千萬小心,在同一項目中存在的但沒用到Shader也要考慮在內(nèi),千萬不要合起來在數(shù)量上超出Unity的關鍵詞數(shù)量限制了。

 


1.5 Unity內(nèi)置的快捷multi_compile指令


如下有Unity內(nèi)置的幾個著色器變體的快捷多編譯指令,他們大多是應對Unity中不同的光線,陰影和光照貼圖類型。詳情見rendering pipeline 。

  • multi_compile_fwdbase - 此指令表示,編譯正向基礎渲染通道(用于正向渲染中,應用環(huán)境光照、主方向光照和頂點/球面調(diào)和光照(Spherical Harmonic Lighting))所需的所有變體。這些變體用于處理不同的光照貼圖類型、主要方向光源的陰影選項的開關與否。
  • multi_compile_fwdadd - 此指令表示, 編譯正向附加渲染通道(用于正向渲染中;以每個光照一個通道的方式應用附加的逐像素光照)所需的所有變體。這些變體用于處理光源的類型(方向光源、聚光燈或者點光源),且這些變種都包含紋理cookie。
  • multi_compile_fwdadd_fullshadows – 此指令和上面的正向渲染附加通道基本一致,但同時為上述通道的處理賦予了光照實時陰影的能力。
  • multi_compile_fog - 此指令表示,編譯出幾個不同的Shader變體來處理不同類型的霧效(關閉/線性/指數(shù)/二階指數(shù))(off/linear/exp/exp2). 

 


1.6 使用指令跳過某些變體的編譯


大多數(shù)內(nèi)置的快捷指令導致了很多著色的變體。若我們熟悉他們且知道有些并非所需,可以使用#pragmaskip_variants語句跳過其中一些的編譯。例如: 

#pragma multi_compile_fwdadd// 將跳過所有使用'POINT'或 'POINT_COOKIE'的變體#pragma skip_variants POINT POINT_COOKIE

OK,通過上面經(jīng)過翻譯&理解過后的官方文檔材料,應該對Unity中的著色器編譯多樣化有了一個理解。說白了,著色器變體的定義和使用與宏定義很類似。

 


1.7 對知識的提煉


上面交代了這么多,看不懂沒關系,我們提煉一下,看懂這段提煉,關于著色器變體的意義與使用方式,也就懂了大半了。

若我們在著色器中定義了這一句:

 

#pragma shader_feature _THIS_IS_A_SAMPLE

這句代碼理解起來,也就是_THIS_IS_A_SAMPLE被我們定義過了,它是存在的,以后我們?nèi)绻袛?ifdef _THIS_IS_A_SAMPLE,那就是真了。我們可以在這個判斷的#ifdef…… #endif塊里面實現(xiàn)自己需要的實現(xiàn)代碼X,這段實現(xiàn)代碼X,只會在你用#pragma multi_compile 或#pragmashader_feature定義了_THIS_IS_A_SAMPLE這個“宏”的時候會被執(zhí)行,否則,它就不會被執(zhí)行到。


實現(xiàn)代碼X的執(zhí)行與不執(zhí)行,全靠你對變體的定義與否。這就是著色器編譯多樣化的實現(xiàn)方式,一個著色器+多個CG頭文件的小團隊(如標準著色器),可以獨當一面,一個打一群,可以取代一大堆獨立實現(xiàn)的Shader的原因所在。


 





二、Standard Shader中正向基礎渲染通道源碼分析

 



這一節(jié)主要用來解析Standard Shader中正向基礎渲染通道的源碼。

先上Standard Shader正向渲染基礎通道(Shader Model 3.0版)的Shader源代碼: 

//------------------------------------【子著色器1】------------------------------------ // 此子著色器用于Shader Model 3.0 //---------------------------------------------------------------------------------------- SubShader { //渲染類型設置:不透明 Tags { 'RenderType'='Opaque' 'PerformanceChecks'='False' } //細節(jié)層次設為:300 LOD 300 //--------------------------------通道1------------------------------- // 正向基礎渲染通道(Base forward pass) // 處理方向光,自發(fā)光,光照貼圖等 ... Pass { //設置通道名稱 Name 'FORWARD' //于通道標簽中設置光照模型為ForwardBase,正向渲染基礎通道 Tags { 'LightMode' = 'ForwardBase' } //混合操作:源混合乘以目標混合 Blend [_SrcBlend] [_DstBlend] // 根據(jù)_ZWrite參數(shù),設置深度寫入模式開關與否 ZWrite [_ZWrite] //===========開啟CG著色器語言編寫模塊=========== CGPROGRAM //著色器編譯目標:Model 3.0 #pragma target 3.0 //編譯指令:不使用GLES渲染器編譯 #pragma exclude_renderers gles // ---------編譯指令:著色器編譯多樣化-------- #pragma shader_feature _NORMALMAP #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON #pragma shader_feature _EMISSION #pragma shader_feature _METALLICGLOSSMAP #pragma shader_feature ___ _DETAIL_MULX2 #pragma shader_feature _PARALLAXMAP //--------著色器編譯多樣化快捷指令------------ //編譯指令:編譯正向渲染基礎通道(用于正向渲染中,應用環(huán)境光照、主方向光照和頂點/球面調(diào)和光照)所需的所有變體。 //這些變體用于處理不同的光照貼圖類型、主要方向光源的陰影選項的開關與否 #pragma multi_compile_fwdbase //編譯指令:編譯幾個不同變種來處理不同類型的霧效(關閉/線性/指數(shù)/二階指數(shù)/) #pragma multi_compile_fog //編譯指令:告知編譯器頂點和片段著色函數(shù)的名稱 #pragma vertex vertForwardBase #pragma fragment fragForwardBase //包含輔助CG頭文件 #include 'UnityStandardCore.cginc' //===========結束CG著色器語言編寫模塊=========== ENDCG } …… }


OK,一起來稍微分析一下上述代碼?;旧鲜侵鹦凶⑨專哉?guī)讉€容易疑惑的點來提一下。

 

第一處,著色器編譯多樣化部分,代碼如下:

 

// ---------編譯指令:著色器編譯多樣化--------#pragma shader_feature _NORMALMAP#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON#pragma shader_feature _EMISSION#pragma shader_feature _METALLICGLOSSMAP#pragma shader_feature ___ _DETAIL_MULX2#pragma shader_feature _PARALLAXMAP


上文剛講過著色器編譯多樣化的一些理解,理解起來就是這樣,這邊定義了很多的“宏”、 _NORMALMAP、_ALPHATEST_ON、_ALPHABLEND_ON、_EMISSION、_METALLICGLOSSMAP、_DETAIL_MULX2、_PARALLAXMAP,在頂點和片段著色器實現(xiàn)部分,可以用#ifdef _EMISSION類似的宏命令來對不同情況下的實現(xiàn)進行區(qū)別對待。

 

第二處,著色器編譯多樣化快捷指令部分,上文的講解部分也有分別提到,這里代碼注釋已經(jīng)很詳細,如下:


//--------著色器編譯多樣化快捷指令------------//編譯指令:編譯正向渲染基礎通道(用于正向渲染中,應用環(huán)境光照、主方向光照和頂點/球面調(diào)和光照)所需的所有變體。//這些變體用于處理不同的光照貼圖類型、主要方向光源的陰影選項的開關與否#pragma multi_compile_fwdbase //編譯指令:編譯幾個不同變種來處理不同類型的霧效(關閉/線性/指數(shù)/二階指數(shù)/)#pragma multi_compile_fog


第三處,頂點著色函數(shù)和片段著色函數(shù)聲明部分,代碼如下: 


//編譯指令:告知編譯器頂點和片段著色函數(shù)的名稱#pragma vertex vertForwardBase#pragma fragment fragForwardBase

這里比較關鍵,指明了這個pass中頂點著色函數(shù)和片段著色函數(shù)分別是名為vertForwardBase和fragForwardBase的函數(shù)。而這兩個函數(shù)定義于何處?看包含頭文件是什么即可。一起來看一下第四處。

 

第四處,CG頭文件包含部分,代碼如下:


//包含輔助CG頭文件#include'UnityStandardCore.cginc'

 

很簡單的一句話,但卻像一切編程語言中頭文件的包含一樣,非常關鍵,不能缺少。vertForwardBase和       fragForwardBase的函數(shù)全都定義于此“UnityStandardCore.cginc”頭文件中。

 

OK,我們轉(zhuǎn)到“UnityStandardCore.cginc”頭文件,繼續(xù)分析下去。先從vertForwardBase函數(shù)開始。



1.頂點著色函數(shù)——vertForwardBase

 

vertForwardBase函數(shù)也已詳細注釋好,代碼如下:

//-----------------------------------【vertForwardBase函數(shù)】----------------------------------------// 用途:正向渲染基礎通道的頂點著色函數(shù)// 說明:實例化一個VertexOutputForwardBase結構體對象,并進行相應的填充// 輸入:VertexInput結構體// 輸出:VertexOutputForwardBase結構體// 附:VertexInput結構體原型:/*struct VertexInput{ float4 vertex : POSITION; half3 normal : NORMAL; float2 uv0 : TEXCOORD0; float2 uv1 : TEXCOORD1; #if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META) float2 uv2 : TEXCOORD2; #endif #ifdef _TANGENT_TO_WORLD half4 tangent : TANGENT; #endif};*///---------------------------------------------------------------------------------------------------------VertexOutputForwardBase vertForwardBase (VertexInput v){ //【1】實例化一個VertexOutputForwardBase結構體對象 VertexOutputForwardBase o; //用Unity內(nèi)置的宏初始化參數(shù) UNITY_INITIALIZE_OUTPUT(VertexOutputForwardBase, o); //【2】通過物體坐標系到世界坐標系的變換矩陣乘以物體的頂點位置,得到對象在世界坐標系中的位置 float4 posWorld = mul(_Object2World, v.vertex); //【3】若定義了鏡面立方體投影宏,將計算得到的世界坐標系的xyz坐標作為輸出參數(shù)的世界坐標值 #if UNITY_SPECCUBE_BOX_PROJECTION o.posWorld = posWorld.xyz; #endif //【4】輸出的頂點位置(像素位置)為模型視圖投影矩陣乘以頂點位置,也就是將三維空間中的坐標投影到了二維窗口 o.pos = mul(UNITY_MATRIX_MVP, v.vertex); //【5】計算紋理坐標,使用UnityStandardInput.cginc頭文件中的輔助函數(shù)。 o.tex = TexCoords(v); //【6】視線的方向= 對象在世界坐標系中的位置減去攝像機的世界空間位置,并進行逐頂點歸一化 o.eyeVec = NormalizePerVertexNormal(posWorld.xyz - _WorldSpaceCameraPos); //【7】計算物體在世界空間中的法線坐標 float3 normalWorld = UnityObjectToWorldNormal(v.normal); //【8】進行世界空間中的切線相關參數(shù)的計算與賦值 //若定義了_TANGENT_TO_WORLD #ifdef _TANGENT_TO_WORLD //世界空間中的物體的法線值 float4 tangentWorld = float4(UnityObjectToWorldDir(v.tangent.xyz), v.tangent.w); //在世界空間中為每個頂點創(chuàng)建切線 float3x3 tangentToWorld = CreateTangentToWorldPerVertex(normalWorld, tangentWorld.xyz, tangentWorld.w); //分別為3個分量賦值 o.tangentToWorldAndParallax[0].xyz = tangentToWorld[0]; o.tangentToWorldAndParallax[1].xyz = tangentToWorld[1]; o.tangentToWorldAndParallax[2].xyz = tangentToWorld[2]; //否則,三個分量直接取為0,0和上面計算得到的normalWorld #else o.tangentToWorldAndParallax[0].xyz = 0; o.tangentToWorldAndParallax[1].xyz = 0; o.tangentToWorldAndParallax[2].xyz = normalWorld; #endif //【9】陰影的獲取 TRANSFER_SHADOW(o); //【10】進行頂點正向相關的全局光照操作 o.ambientOrLightmapUV = VertexGIForward(v, posWorld, normalWorld); //【11】若定義了_PARALLAXMAP宏,則計算視差的視角方向并賦值 #ifdef _PARALLAXMAP //聲明一個由切線空間的基組成的3x3矩陣“rotation” TANGENT_SPACE_ROTATION; //計算視差的視角方向 half3 viewDirForParallax = mul (rotation, ObjSpaceViewDir(v.vertex)); //分別將三個分量賦值給VertexOutputForwardBase結構體對象o的tangentToWorldAndParallax的三個分量 o.tangentToWorldAndParallax[0].w = viewDirForParallax.x; o.tangentToWorldAndParallax[1].w = viewDirForParallax.y; o.tangentToWorldAndParallax[2].w = viewDirForParallax.z; #endif //【12】若定義了UNITY_OPTIMIZE_TEXCUBELOD,便計算反射光方向向量并賦值 #if UNITY_OPTIMIZE_TEXCUBELOD //使用CG語言內(nèi)置函數(shù)reflect計算反射光方向向量 o.reflUVW = reflect(o.eyeVec, normalWorld); #endif //【13】從頂點中輸出霧數(shù)據(jù) UNITY_TRANSFER_FOG(o,o.pos); //【14】返回已經(jīng)附好值的VertexOutputForwardBase類型的對象 return o;}


基本步驟已經(jīng)在代碼注釋中用序號列出,以下將對其中的主要知識點進行講解。首先看一下函數(shù)的輸出參數(shù)——VertexInput。



2.頂點輸入結構體——VertexInput


此結構體定義于UnityStandardInput.cginc頭文件中,是頂點著色函數(shù)vertForwardBase的輸入?yún)?shù),相關代碼如下所示:

//頂點輸入結構體struct VertexInput{ float4 vertex : POSITION;//位置坐標 half3 normal : NORMAL;//法線向量 float2 uv0 : TEXCOORD0;//一級紋理坐標 float2 uv1 : TEXCOORD1;//二級紋理坐標 //若DYNAMICLIGHTMAP_ON或者UNITY_PASS_META選項為開,則還定義一個三級紋理#if defined(DYNAMICLIGHTMAP_ON) || defined(UNITY_PASS_META) float2 uv2 : TEXCOORD2;//三級紋理#endif#ifdef _TANGENT_TO_WORLD half4 tangent : TANGENT;//切線向量#endif};


此結構體比較通用,不僅僅是用于正向基礎渲染通道,畢竟是定義在UnityStandardInput.cginc頭文件中的。

各個變量的含義,注釋中已經(jīng)寫到了,好像沒有什么值得多說的,再來看下頂點輸出結構體。

 


3.頂點輸出結構體——VertexOutputForwardBase


顧名思義,VertexOutputForwardBase結構體就是正向基礎渲染通道特有的輸出結構體,定義于UnityStandardCore.cginc頭文件中,注釋后的代碼如下:

 

//正向渲染基礎通道的輸出結構體struct VertexOutputForwardBase{ float4 pos : SV_POSITION;//像素坐標 float4 tex : TEXCOORD0;//一級紋理 half3 eyeVec : TEXCOORD1;//二級紋理(視線向量) half4 tangentToWorldAndParallax[3] : TEXCOORD2; //3x3為切線到世界矩陣的值,1x3為視差方向的值 half4 ambientOrLightmapUV : TEXCOORD5; // 球諧函數(shù)(Spherical harmonics)或光照貼圖的UV坐標 SHADOW_COORDS(6)//陰影坐標 UNITY_FOG_COORDS(7)//霧效坐標 //若定義了鏡面立方體投影宏,定義一個posWorld #if UNITY_SPECCUBE_BOX_PROJECTION float3 posWorld : TEXCOORD8; #endif //若定義了優(yōu)化紋理的立方體LOD宏,還將定義如下的參數(shù)reflUVW #if UNITY_OPTIMIZE_TEXCUBELOD #if UNITY_SPECCUBE_BOX_PROJECTION half3 reflUVW : TEXCOORD9; #else half3 reflUVW : TEXCOORD8; #endif #endif};


從這里開始,做一個規(guī)定,為了方便對照和理解,以下貼出代碼中也會貼出原始的英文注釋——先翻譯為中文,以 || 結束,在 || 后附上原始的英文。

就像這樣:

 //最終的二次多項式 ||  Final quadraticpolynomial


OK,我們繼續(xù),vertForwardBase函數(shù)中有很多知識點值得拿出來講一講的。



4. UNITY_INITIALIZE_OUTPUT宏

 

UNITY_INITIALIZE_OUTPUT(type,name) –此宏用于將給定類型的名稱變量初始化為零。在使用舊版標準所寫的Shader時,經(jīng)常會報錯“Try adding UNITY_INITIALIZE_OUTPUT(Input,o); like this in your vertfunction.”之類的錯誤,加上這句就不會報錯了。

 


5._Object2World矩陣

 

_Object2World,Unity的內(nèi)置矩陣,世界坐標系到對象坐標系的變換矩陣,簡稱“世界-對象矩陣”。

 

 

6.UNITY_MATRIX_MVP矩陣

 

UNITY_MATRIX_MVP為當前的模型矩陣x視圖矩陣x投影矩陣,簡稱“模型-視圖-投影矩陣”。其常用于在頂點著色函數(shù)中,通過將它和頂點位置相乘,從而可以把頂點位置從模型空間轉(zhuǎn)換到裁剪空間(clip space)中。也就是通過此矩陣,將三維空間中的坐標投影到了二維窗口中。

 

 

7.TexCoords函數(shù)


TexCoords函數(shù)用于獲取紋理坐標,定義UnityStandardInput.cginc頭文件中,相關代碼如下:

     

float4 TexCoords(VertexInput v){ float4 texcoord; texcoord.xy = TRANSFORM_TEX(v.uv0, _MainTex); // Always source from uv0 texcoord.zw = TRANSFORM_TEX(((_UVSec == 0) ? v.uv0 : v.uv1), _DetailAlbedoMap); return texcoord;}

函數(shù)實現(xiàn)代碼中的_MainTex、_UVSec、_DetailAlbedoMap都是此頭文件定義的全局的變量。 

其中還涉及到了一個TRANSFORM_TEX宏,在這邊也提一下,它定義于UnityCG.cginc頭文件中,相關代碼如下:

 

// 按比例和偏移進行二維UV坐標的變換#define TRANSFORM_TEX(tex,name) (tex.xy *name##_ST.xy + name##_ST.zw)


 

8. NormalizePerVertexNormal函數(shù)

 

此函數(shù)位于unitystandardcore.cginc頭文件中,原型和注釋如下:

//--------------------------【函數(shù)NormalizePerVertexNormal】-----------------------------// 用途:歸一化每頂點法線// 說明:若滿足特定條件,便歸一化每頂點法線并返回,否則,直接返回原始值// 輸入:half3類型的法線坐標// 輸出:若滿足判斷條件,返回half3類型的、經(jīng)過歸一化后的法線坐標,否則返回輸入的值//----------------------------------------------------------------------------------------------- half3 NormalizePerVertexNormal (half3 n){ //滿足著色目標模型的版本小于Shader Model 3.0,或者定義了UNITY_STANDARD_SIMPLE宏,返回歸一化后的值 #if (SHADER_TARGET < 30)="" ||="" unity_standard_simple="" return="" normalize(n);="" 否則,直接返回輸入的參數(shù),后續(xù)應該會進行逐像素的歸一化="" #else="" return="" n;="">

其中,SHADER_TARGET宏代表的值為和著色器的目標編譯模型(shader model)相關的一個數(shù)值。

例如,當著色器編譯成Shader Model 3.0時,SHADER_TARGET 便為30。我們可以在shader代碼中由此來進行條件判斷。相關代碼如下:


#if SHADER_TARGET < 30//實現(xiàn)代碼a#else="">



9. UnityObjectToWorldNormal函數(shù)


UnityObjectToWorldNormal是Unity內(nèi)置的函數(shù),可以將法線從模型空間變換到世界空間中,定義于UnityCG.cginc頭文件中,相關代碼如下:

//將法線從模型空間變換到世界空間inline float3 UnityObjectToWorldNormal( in float3 norm ){ // 將分量分別相乘,并進行歸一化 //Multiply by transposed inverse matrix, actually using transpose() generates badly optimized code return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z);}


而其中的normalize( )函數(shù)太常見不過了,是來自CG語言中的函數(shù),作用是歸一化向量。

 



10.UnityObjectToWorldDir函數(shù)


UnityObjectToWorldDir函數(shù)用于方向值從物體空間切換到世界空間,也定義于UnityCG.cginc頭文件中,相關代碼如下:

//將方向值從物體空間切換到世界空間inline float3 UnityObjectToWorldDir( in float3 dir ){ return normalize(mul((float3x3)_Object2World, dir));}

可以看到,就是返回一個世界-對象矩陣乘以方向值歸一化后的結果,比較好理解。

 



11. CreateTangentToWorldPerVertex函數(shù)


CreateTangentToWorldPerVertex函數(shù)用于在世界空間中為每個頂點創(chuàng)建切線,定義于UnityStandardUtils.cginc頭文件中,相關代碼如下:

half3x3 CreateTangentToWorldPerVertex(half3 normal, half3 tangent, half tangentSign){ //對于奇數(shù)負比例變換,我們需要將符號反向||For odd-negative scale transforms we need to flip the sign half sign = tangentSign * unity_WorldTransformParams.w; half3 binormal = cross(normal, tangent) * sign; return half3x3(tangent, binormal, normal);}

其中的unity_WorldTransformParams是UnityShaderVariables.cginc頭文件中定義的一個uniform float4型的變量,其w分量用于標定奇數(shù)負比例變換(odd-negativescale transforms),通常取值為1.0或者-1.0。

 





12.TRANSFER_SHADOW(a)宏



此宏用于進行陰影在各種空間中的轉(zhuǎn)換,定義于AutoLight.cginc中。在不同的情況下,此宏代表的意義并不相同。下面簡單進行下展開分析。

 

1)對于屏幕空間中的陰影(Screen space shadows)

對應于屏幕空間中的陰影,也就是#if defined (SHADOWS_SCREEN),其相關代碼如下:

#if defined (SHADOWS_SCREEN)……#if defined(UNITY_NO_SCREENSPACE_SHADOWS)#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );#else // not UNITY_NO_SCREENSPACE_SHADOWS#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);……#endif

也就是說,這種情況下的TRANSFER_SHADOW(a)宏,代表了一句代碼,這句代碼就是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));

此句代碼的含義是:將世界-陰影坐標乘以世界-模型坐標和物體頂點坐標的積,也就是先將物體坐標轉(zhuǎn)換成世界坐標,再將世界坐標轉(zhuǎn)換成陰影坐標,并將結果存放于a._ShadowCoord中。

 


2)對于聚光燈陰影(Spot light shadows)

而對于聚光燈的陰影,也就是#if defined (SHADOWS_DEPTH)&& defined (SPOT)

有如下定義:

#if defined (SHADOWS_DEPTH) && defined (SPOT)#define TRANSFER_SHADOW(a) a._ShadowCoord = mul (unity_World2Shadow[0], mul(_Object2World,v.vertex));……#endif

可以發(fā)現(xiàn),這種情況下的TRANSFER_SHADOW(a)宏代表的語句也是a._ShadowCoord = mul (unity_World2Shadow[0],mul(_Object2World,v.vertex));

同上,用途就是先將物體坐標轉(zhuǎn)換成世界坐標,再將世界坐標轉(zhuǎn)換成陰影坐標,并將結果存放于a._ShadowCoord中。

 


3)對于點光源陰影(Point light shadows)


而對于點光源的陰影,也就是#if defined (SHADOWS_CUBE),有如下定義:

 

#if defined (SHADOWS_CUBE) #define TRANSFER_SHADOW(a) a._ShadowCoord = mul(_Object2World, v.vertex).xyz - _LightPositionRange.xyz;……#endif

也就是說,這種情況下的TRANSFER_SHADOW(a)宏代表語句a._ShadowCoord = mul(_Object2World, v.vertex).xyz -_LightPositionRange.xyz;

想了解此代碼的含義,先要知道_LightPositionRange變量的含義。

這個變量是UnityShaderVariables.cginc頭文件中定義的一個全局變量:

uniform float4 _LightPositionRange; // xyz= pos, w = 1/range

從英文注釋可以發(fā)現(xiàn),此參數(shù)的x,y,z分量表示世界空間下光源的坐標,而w為世界空間下范圍的倒數(shù)。

那么此句代碼的含義,也就是先將物體-世界矩陣乘以物體頂點坐標,得到物體的世界空間坐標,然后取坐標的xyz分量,與光源的坐標相減,并將結果賦給a._ShadowCoord。

 


4)對于關閉陰影(Shadows off)的情況


而對于關閉陰影的情況,也就是#if !defined (SHADOWS_SCREEN)&& !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE),有如下定義:

 

#if !defined (SHADOWS_SCREEN) && !defined (SHADOWS_DEPTH) && !defined (SHADOWS_CUBE) #define TRANSFER_SHADOW(a)……#endif

這種情況下的TRANSFER_SHADOW(a)宏代表的是空白,并沒有什么用。

 

 


13. VertexGIForward函數(shù)


定義于UnityStandardCore.cginc頭文件中。詳細注釋后的代碼如下:

//頂點正向全局光照函數(shù)inline half4 VertexGIForward(VertexInput v, float3 posWorld, half3 normalWorld){ //【1】定義一個half4型的ambientOrLightmapUV變量,并將四個分量都置為0 half4 ambientOrLightmapUV = 0; //【2】對ambientOrLightmapUV變量的四個分量賦值 // 【2-1】若沒有定義LIGHTMAP_OFF(關閉光照貼圖)宏,也就是此情況下啟用靜態(tài)的光照貼圖,則計算對應的光照貼圖坐標 //static lightmap #ifndef LIGHTMAP_OFF ambientOrLightmapUV.xy = v.uv1.xy * unity_LightmapST.xy + unity_LightmapST.zw; ambientOrLightmapUV.zw = 0; //【2-2】若定義了UNITY_SHOULD_SAMPLE_SH宏,則表示對動態(tài)的對象采樣(不對靜態(tài)或者動態(tài)的光照貼圖采樣) // || Sample light probe for Dynamic objects only (no static or dynamic lightmaps) #elif UNITY_SHOULD_SAMPLE_SH //【2-2-1】若定義了如下的UNITY_SAMPLE_FULL_SH_PER_PIXEL宏(即采樣計算全部的每像素球面調(diào)和光照),便給ambientOrLightmapUV.rgb賦值為0 #if UNITY_SAMPLE_FULL_SH_PER_PIXEL ambientOrLightmapUV.rgb = 0; //【2-2-2】若滿足著色目標模型的版本小于Shader Model 3.0,或者定義了UNITY_STANDARD_SIMPLE宏 //便使用球面調(diào)和函數(shù)ShadeSH9給ambientOrLightmapUV.rgb賦值 #elif (SHADER_TARGET < 30)="" ||="" unity_standard_simple="" ambientorlightmapuv.rgb="ShadeSH9(half4(normalWorld," 1.0));="" 【2-2-3】否則,使用三序球面調(diào)和函數(shù)shadesh3order給ambientorlightmapuv.rgb賦值="" #else="" 優(yōu)化操作:光源l0、l1逐像素,光源l2逐頂點="" ||="" optimization:="" l2="" per-vertex,="" l0..l1="" per-pixel="" ambientorlightmapuv.rgb="ShadeSH3Order(half4(normalWorld," 1.0));="" #endif="" 【2-2-4】="" 從非重要的點光源中添加近似的照明="" ||="" add="" approximated="" illumination="" from="" non-important="" point="" lights="" 若定義了如下的vertexlight_on宏(即開啟頂點光照),便使用shade4pointlights函數(shù)給ambientorlightmapuv.rgb賦值,添加環(huán)境光="" #ifdef="" vertexlight_on="" shade4pointlights為unity內(nèi)置的逐頂點光照處理函數(shù),定義于unitycg.cginc頭文件中="" ambientorlightmapuv.rgb="" +="Shade4PointLights" (="" unity_4lightposx0,="" unity_4lightposy0,="" unity_4lightposz0,="" unity_lightcolor[0].rgb,="" unity_lightcolor[1].rgb,="" unity_lightcolor[2].rgb,="" unity_lightcolor[3].rgb,="" unity_4lightatten0,="" posworld,="" normalworld);="" #endif="" #endif="" 【2-3】若定義了如下的vertexlight_ondynamiclightmap_on宏(即開啟動態(tài)光照貼圖),則給變量的zw分量賦值="" #ifdef="" dynamiclightmap_on="" ambientorlightmapuv.zw="v.uv2.xy" *="" unity_dynamiclightmapst.xy="" +="" unity_dynamiclightmapst.zw;="" #endif="" 【3】返回ambientorlightmapuv變量的值="" return="">

其中有一些小的點,這邊提出來講一下。

1)unity_LightmapST變量

unity_LightmapST變量類型為float4型,定義于UnityShaderVariables.cginc頭文件中,存放著光照貼圖操作的參數(shù)的值:

float4 unity_LightmapST;


2)UNITY_SHOULD_SAMPLE_SH宏

此宏定義于UnityCG.cginc中,相關代碼如下:

//包含間接漫反射的動態(tài)&靜態(tài)光照貼圖,所以忽略掉球面調(diào)和光照 || Dynamic & Static lightmaps contain indirect diffuse ligthing, thus ignore SH#define UNITY_SHOULD_SAMPLE_SH ( defined (LIGHTMAP_OFF) && defined(DYNAMICLIGHTMAP_OFF) )

可以發(fā)現(xiàn),這個宏,其實就是將LIGHTMAP_OFF(關閉光照貼圖)宏和DYNAMICLIGHTMAP_OFF(關閉動態(tài)光照貼圖)宏的定義進行了封裝。

 

3)UNITY_SAMPLE_FULL_SH_PER_PIXEL宏

UNITY_SAMPLE_FULL_SH_PER_PIXEL宏定義于UnityStandardConfig.cginc頭文件中。其實也就是一個標識符,用0標示UNITY_SAMPLE_FULL_SH_PER_PIXEL宏是否已經(jīng)定義。按字面上理解,啟用此宏表示我們將采樣計算每像素球面調(diào)和光照,而不是默認的逐頂點計算球面調(diào)和光照并且線性插值到每像素中。其實現(xiàn)代碼如下,非常簡單:

#ifndef UNITY_SAMPLE_FULL_SH_PER_PIXEL#define UNITY_SAMPLE_FULL_SH_PER_PIXEL 0#endif


4)ShadeSH9函數(shù)


ShadeSH9就是大家常說的球面調(diào)和函數(shù),定義于UnityCG.cginc頭文件中,相關代碼如下:

//球面調(diào)和函數(shù)//法線需被初始化,w=1.0 || normal should be normalized, w=1.0half3 ShadeSH9 (half4 normal){ half3 x1, x2, x3; //線性+常數(shù)多項式 || Linear + constant polynomial terms x1.r = dot(unity_SHAr,normal); x1.g = dot(unity_SHAg,normal); x1.b = dot(unity_SHAb,normal); //二次多項式的四個參數(shù) || 4 of the quadratic polynomials half4 vB = normal.xyzz * normal.yzzx; x2.r = dot(unity_SHBr,vB); x2.g = dot(unity_SHBg,vB); x2.b = dot(unity_SHBb,vB); //最終二次多項式 || Final quadratic polynomial half vC = normal.x*normal.x - normal.y*normal.y; x3 = unity_SHC.rgb * vC; return x2 + x3 + x1;}

 

5)ShadeSH3Order函數(shù)

ShadeSH3Order函數(shù),我將其翻譯為三序球面調(diào)和函數(shù)。定義于UnityCG.cginc頭文件中,相關代碼如下:

//三序球面調(diào)和函數(shù)//法線需被初始化,w=1.0 || normal should be normalized, w=1.0half3 ShadeSH3Order(half4 normal){ half3 x2, x3; //二次多項式的四個參數(shù) || 4 of the quadratic polynomials half4 vB = normal.xyzz * normal.yzzx; x2.r = dot(unity_SHBr,vB); x2.g = dot(unity_SHBg,vB); x2.b = dot(unity_SHBb,vB); //最終的二次多項式 || Final quadratic polynomial half vC = normal.x*normal.x - normal.y*normal.y; x3 = unity_SHC.rgb * vC; return x2 + x3;}



6)Shade4PointLights函數(shù)

Shade4PointLights為Unity為我們準備好的逐頂點光照處理函數(shù),定義于unityCG.cginc頭文件中,相關代碼如下:

//在正向基礎渲染通道中使用,根據(jù)4個不同的點光源計算出漫反射光照參數(shù)的rgb值|| Used in ForwardBase pass: Calculates diffuse lighting from 4 point lights, with data packed in a special way.float3 Shade4PointLights ( float4 lightPosX, float4 lightPosY, float4 lightPosZ, float3 lightColor0, float3 lightColor1, float3 lightColor2, float3 lightColor3, float4 lightAttenSq, float3 pos, float3 normal){ // 【1】將輸入?yún)?shù)轉(zhuǎn)換為光照矢量 || to light vectors float4 toLightX = lightPosX - pos.x; float4 toLightY = lightPosY - pos.y; float4 toLightZ = lightPosZ - pos.z; // 【2】計算平方的值 || squared lengths float4 lengthSq = 0; lengthSq += toLightX * toLightX; lengthSq += toLightY * toLightY; lengthSq += toLightZ * toLightZ; // 【3】法線方向點乘光線方向|| NdotL float4 ndotl = 0; ndotl += toLightX * normal.x; ndotl += toLightY * normal.y; ndotl += toLightZ * normal.z; // 【4】修正NdotL(法線方向點乘光線方向)的值 || correct NdotL float4 corr = rsqrt(lengthSq); ndotl = max (float4(0,0,0,0), ndotl * corr); // 【5】計算衰減系數(shù) || attenuation float4 atten = 1.0 / (1.0 + lengthSq * lightAttenSq); float4 diff = ndotl * atten; // 【6】得到最終的顏色 || final color float3 col = 0; col += lightColor0 * diff.x; col += lightColor1 * diff.y; col += lightColor2 * diff.z; col += lightColor3 * diff.w; return col;}




14. TANGENT_SPACE_ROTATION宏


TANGENT_SPACE_ROTATION宏定義于UnityCG.cginc中,作用是聲明一個由切線空間的基組成的3x3矩陣,相關代碼如下:

 

//聲明一個由切線空間的基組成的3x3矩陣 || Declares 3x3 matrix 'rotation', filled with tangent space basis#define TANGENT_SPACE_ROTATION \ float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \ float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

也就是說,使用TANGENT_SPACE_ROTATION宏也就表示定義了上述代碼所示的float3 類型的binormal和float3x3類型的rotation兩個變量。且其中的rotation為3x3的矩陣,由切線空間的基組成??梢允褂盟盐矬w空間轉(zhuǎn)換到切線空間中。

 



15.UNITY_OPTIMIZE_TEXCUBELOD宏


UNITY_OPTIMIZE_TEXCUBELOD宏的定義非常簡單,就是用0標識是否開啟此功能,如下所示:

#ifndef UNITY_OPTIMIZE_TEXCUBELOD #define UNITY_OPTIMIZE_TEXCUBELOD 0#endif



16.reflect函數(shù)

 

reflect函數(shù)是CG語言的內(nèi)置函數(shù)。

reflect(I, N) 根據(jù)入射光方向向量I,和頂點法向量N,計算反射光方向向量。其中I 和N必須被歸一化,需要特別注意的是,這個I 是指向頂點的;且此函數(shù)只對三元向量有效。

 

 

17.UNITY_TRANSFER_FOG宏


UNITY_TRANSFER_FOG宏相關代碼定義于UnityCG.Cginc頭文件中,用于的相關代碼如下所示:

//【0】實現(xiàn)不同版本的UNITY_CALC_FOG_FACTOR宏。#if defined(FOG_LINEAR) // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = (coord) * unity_FogParams.z + unity_FogParams.w#elif defined(FOG_EXP) // factor = exp(-density*z) #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.y * (coord); unityFogFactor = exp2(-unityFogFactor)#elif defined(FOG_EXP2) // factor = exp(-(density*z)^2) #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = unity_FogParams.x * (coord); unityFogFactor = exp2(-unityFogFactor*unityFogFactor)#else #define UNITY_CALC_FOG_FACTOR(coord) float unityFogFactor = 0.0#endif//【1】若已經(jīng)定義了FOG_LINEAR、FOG_EXP、FOG_EXP2宏三者之中至少之一,便可以進行到此#if實現(xiàn)部分#if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2) //【1-1】定義UNITY_FOG_COORDS(idx)宏 #define UNITY_FOG_COORDS(idx) float fogCoord : TEXCOORD##idx; //【1-2】定義UNITY_TRANSFER_FOG(o,outpos)宏 //【1-2-1】若滿足著色目標模型的版本小于Shader Model 3.0,或者定義了SHADER_API_MOBILE宏,便可以進行到此#if實現(xiàn)部分//UNITY_CALC_FOG_FACTOR宏的實現(xiàn)見上 #if (SHADER_TARGET < 30)="" ||="" defined(shader_api_mobile)="" 移動平臺和shader="" model="" 2.0:計算每頂點的霧效因子="" ||="" mobile="" or="" sm2.0:="" calculate="" fog="" factor="" per-vertex="" #define="" unity_transfer_fog(o,outpos)="" unity_calc_fog_factor((outpos).z);="" o.fogcoord="unityFogFactor" 【1-2-2】否則="" #else="" shader="" model="" 3.0和pc/游戲主機平臺:計算每頂點的霧距離,以及每像素霧效因子="" ||="" sm3.0="" and="" pc/console:="" calculate="" fog="" distance="" per-vertex,="" and="" fog="" factor="" per-pixel="" #define="" unity_transfer_fog(o,outpos)="" o.fogcoord="(outpos).z" #endif//【2】否則,直接用unity_fog_coords宏計算霧效參數(shù)#else="" #define="" unity_fog_coords(idx)="" #define="">


可以發(fā)現(xiàn),關于此宏的定義,主要集中在如下幾句:


#if (SHADER_TARGET < 30)="" ||="" defined(shader_api_mobile)="" 移動平臺和shader="" model="" 2.0:計算每頂點的霧效因子="" ||="" mobile="" or="" sm2.0:="" calculate="" fog="" factor="" per-vertex="" #define="" unity_transfer_fog(o,outpos)="" unity_calc_fog_factor((outpos).z);="" o.fogcoord="unityFogFactor" 【1-2-2】否則="" #else="" shader="" model="" 3.0和pc/游戲主機平臺:計算每頂點的霧距離,以及每像素霧效因子="" ||="" sm3.0="" and="" pc/console:="" calculate="" fog="" distance="" per-vertex,="" and="" fog="" factor="" per-pixel="" #define="" unity_transfer_fog(o,outpos)="" o.fogcoord="(outpos).z">




而其中宏定義依賴的UNITY_CALC_FOG_FACTOR宏,定義于這段代碼的一開頭,也根據(jù)不同的場合,計算方法分為了幾個版本。

OK,頂點著色器分析完篇幅都這么多了,這一節(jié)就到這里。

 






三、屏幕油畫特效的實現(xiàn)




之前的文章中提出,Unity中的屏幕特效通常分為兩部分來實現(xiàn):

  • Shader實現(xiàn)部分
  • 腳本實現(xiàn)部分

下面依舊是從這兩個方面對本次的特效進行實現(xiàn)。

 



3.1 Shader實現(xiàn)部分

 

依舊老規(guī)矩,先上注釋好的Shader代碼。

//Reference:https://www./view/MsXSRN#Shader '淺墨Shader編程/Volume10/OilPaintEffect' { //------------------------------------【屬性值】------------------------------------ Properties { _MainTex('Base (RGB)', 2D) = 'white' {} _Distortion('_Distortion', Range(0.0, 1.0)) = 0.3 _ScreenResolution('_ScreenResolution', Vector) = (0., 0., 0., 0.) _ResolutionValue('_ResolutionValue', Range(0.0, 5.0)) = 1.0 _Radius('_Radius', Range(0.0, 5.0)) = 2.0 } //------------------------------------【唯一的子著色器】------------------------------------ SubShader { //--------------------------------唯一的通道------------------------------- Pass { //設置深度測試模式:渲染所有像素.等同于關閉透明度測試(AlphaTest Off) ZTest Always //===========開啟CG著色器語言編寫模塊=========== CGPROGRAM //編譯指令: 指定著色器編譯目標為Shader Model 3.0 #pragma target 3.0 //編譯指令:告知編譯器頂點和片段著色函數(shù)的名稱 #pragma vertex vert #pragma fragment frag //包含輔助CG頭文件 #include 'UnityCG.cginc' //外部變量的聲明 uniform sampler2D _MainTex; uniform float _Distortion; uniform float4 _ScreenResolution; uniform float _ResolutionValue; uniform int _Radius; //頂點輸入結構 struct vertexInput { float4 vertex : POSITION;//頂點位置 float4 color : COLOR;//顏色值 float2 texcoord : TEXCOORD0;//一級紋理坐標 }; //頂點輸出結構 struct vertexOutput { half2 texcoord : TEXCOORD0;//一級紋理坐標 float4 vertex : SV_POSITION;//像素位置 fixed4 color : COLOR;//顏色值 }; //--------------------------------【頂點著色函數(shù)】----------------------------- // 輸入:頂點輸入結構體 // 輸出:頂點輸出結構體 //--------------------------------------------------------------------------------- vertexOutput vert(vertexInput Input) { //【1】聲明一個輸出結構對象 vertexOutput Output; //【2】填充此輸出結構 //輸出的頂點位置為模型視圖投影矩陣乘以頂點位置,也就是將三維空間中的坐標投影到了二維窗口 Output.vertex = mul(UNITY_MATRIX_MVP, Input.vertex); //輸出的紋理坐標也就是輸入的紋理坐標 Output.texcoord = Input.texcoord; //輸出的顏色值也就是輸入的顏色值 Output.color = Input.color; //【3】返回此輸出結構對象 return Output; } //--------------------------------【片段著色函數(shù)】----------------------------- // 輸入:頂點輸出結構體 // 輸出:float4型的顏色值 //--------------------------------------------------------------------------------- float4 frag(vertexOutput Input) : COLOR { //【1】根據(jù)設置的分辨率比值,計算圖像尺寸 float2 src_size = float2(_ResolutionValue / _ScreenResolution.x, _ResolutionValue / _ScreenResolution.y); //【2】獲取坐標值 float2 uv = Input.texcoord.xy; //【3】根據(jù)半徑,計算出n的值 float n = float((_Radius + 1) * (_Radius + 1));; //【4】定義一些參數(shù) float3 m0 = 0.0; float3 m1 = 0.0; float3 s0 = 0.0; float3 s1 = 0.0; float3 c; //【5】按半徑Radius的值,迭代計算m0和s0的值 for (int j = -_Radius; j <= 0;="" ++j)="" {="" for="" (int="" i="-_Radius;" i=""><= 0;="" ++i)="" {="" c="tex2D(_MainTex," uv="" +="" float2(i,="" j)="" *="" src_size).rgb;="" m0="" +="c;" s0="" +="c" *="" c;="" }="" }="" 【6】按半徑radius的值,迭代計算m1和s1的值="" for="" (int="" j="0;" j=""><= _radius;="" ++j)="" {="" for="" (int="" i="0;" i=""><= _radius;="" ++i)="" {="" c="tex2D(_MainTex," uv="" +="" float2(i,="" j)="" *="" src_size).rgb;="" m1="" +="c;" s1="" +="c" *="" c;="" }="" }="" 【7】定義參數(shù),準備計算最終的顏色值="" float4="" finalfragcolor="0.;" float="" min_sigma2="1e+2;" 【8】根據(jù)m0和s0,第一次計算finalfragcolor的值="" m0="" n;="" s0="abs(s0" n="" -="" m0="" *="" m0);="" float="" sigma2="s0.r" +="" s0.g="" +="" s0.b;="" if="" (sigma2="">< min_sigma2)="" {="" min_sigma2="sigma2;" finalfragcolor="float4(m0," 1.0);="" }="" 【9】根據(jù)m1和s1,第二次計算finalfragcolor的值="" m1="" n;="" s1="abs(s1" n="" -="" m1="" *="" m1);="" sigma2="s1.r" +="" s1.g="" +="" s1.b;="" if="" (sigma2="">< min_sigma2)="" {="" min_sigma2="sigma2;" finalfragcolor="float4(m1," 1.0);="" }="" 【10】返回最終的顏色值="" return="" finalfragcolor;="" }="" endcg="" }="">

 

需要注意,本次油畫效果的思路來自于Shadertoy中的一個油畫效果的實現(xiàn):https://www./view/MsXSRN#。

此Shadertoy頁面貼出的基于GLSL的Shader代碼的void mainImage( out vec4 fragColor,in vec2 fragCoord )函數(shù)對應于Unity 中Shader的片段著色器。本次Shader中片段著色函數(shù)中的實現(xiàn)方法基本由Shadertoy中的這個OilPaint shader優(yōu)化和精簡而來,具體原理應該估計要翻國外的paper來寫,會花費不少的時間,精力有限,在這邊就暫且不細展開了。暫時只需知道這邊就是在片段著色器用類似濾波的操作計算出了不同的顏色值并輸出即可。

另外需要注意一點,此Shader的_Radius值越大,此Shader就越耗時,因為_Radius決定了雙層循環(huán)的次數(shù),而且是指數(shù)級的決定關系。_Radius值約小,循環(huán)的次數(shù)就會越小,從而有更快的運行效率。

 

 

 

3.2 C#腳本實現(xiàn)部分

 

C#腳本文件的代碼幾乎可以從之前的幾個特效中重用,只用稍微改一點細節(jié)就可以。下面也是貼出詳細注釋的實現(xiàn)此特效的C#腳本:

using UnityEngine;using System.Collections;//設置在編輯模式下也執(zhí)行該腳本[ExecuteInEditMode]//添加選項到菜單中[AddComponentMenu('淺墨Shader編程/Volume10/ScreenOilPaintEffect')]public class ScreenOilPaintEffect : MonoBehaviour { //-------------------變量聲明部分------------------- #region Variables //著色器和材質(zhì)實例 public Shader CurShader; private Material CurMaterial; //兩個參數(shù)值 [Range(0, 5),Tooltip('分辨率比例值')] public float ResolutionValue = 0.9f; [Range(1, 30),Tooltip('半徑的值,決定了迭代的次數(shù)')] public int RadiusValue = 5; //兩個用于調(diào)節(jié)參數(shù)的中間變量 public static float ChangeValue; public static int ChangeValue2; #endregion //-------------------------材質(zhì)的get&set---------------------------- #region MaterialGetAndSet Material material { get { if(CurMaterial == null) { CurMaterial = new Material(CurShader); CurMaterial.hideFlags = HideFlags.HideAndDontSave; } return CurMaterial; } } #endregion //-----------------------------------------【Start()函數(shù)】--------------------------------------------- // 說明:此函數(shù)僅在Update函數(shù)第一次被調(diào)用前被調(diào)用 //-------------------------------------------------------------------------------------------------------- void Start () { //依次賦值 ChangeValue = ResolutionValue; ChangeValue2 = RadiusValue; //找到當前的Shader文件 CurShader = Shader.Find('淺墨Shader編程/Volume10/ScreenOilPaintEffect'); //判斷當前設備是否支持屏幕特效 if(!SystemInfo.supportsImageEffects) { enabled = false; return; } } //-------------------------------------【OnRenderImage()函數(shù)】------------------------------------ // 說明:此函數(shù)在當完成所有渲染圖片后被調(diào)用,用來渲染圖片后期效果 //-------------------------------------------------------------------------------------------------------- void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture) { //著色器實例不為空,就進行參數(shù)設置 if(CurShader != null) { //給Shader中的外部變量賦值 material.SetFloat('_ResolutionValue', ResolutionValue); material.SetInt('_Radius', RadiusValue); material.SetVector('_ScreenResolution', new Vector4(sourceTexture.width, sourceTexture.height, 0.0f, 0.0f)); //拷貝源紋理到目標渲染紋理,加上我們的材質(zhì)效果 Graphics.Blit(sourceTexture, destTexture, material); } //著色器實例為空,直接拷貝屏幕上的效果。此情況下是沒有實現(xiàn)屏幕特效的 else { //直接拷貝源紋理到目標渲染紋理 Graphics.Blit(sourceTexture, destTexture); } } //-----------------------------------------【OnValidate()函數(shù)】-------------------------------------- // 說明:此函數(shù)在編輯器中該腳本的某個值發(fā)生了改變后被調(diào)用 //-------------------------------------------------------------------------------------------------------- void OnValidate() { //將編輯器中的值賦值回來,確保在編輯器中值的改變立刻讓結果生效 ChangeValue = ResolutionValue; ChangeValue2 = RadiusValue; } // Update is called once per frame void Update () { //若程序在運行,進行賦值 if (Application.isPlaying) { //賦值 ResolutionValue = ChangeValue; RadiusValue=ChangeValue2; } //若程序沒有在運行,去尋找對應的Shader文件 #if UNITY_EDITOR if (Application.isPlaying!=true) { CurShader = Shader.Find('淺墨Shader編程/Volume10/ScreenOilPaintEffect'); } #endif } //-----------------------------------------【OnDisable()函數(shù)】--------------------------------------- // 說明:當對象變?yōu)椴豢捎没蚍羌せ顮顟B(tài)時此函數(shù)便被調(diào)用 //-------------------------------------------------------------------------------------------------------- void OnDisable () { if(CurMaterial) { //立即銷毀材質(zhì)實例 DestroyImmediate(CurMaterial); } }}

而根據(jù)腳本中參數(shù)的設定,就有分辨率和半徑兩個參數(shù)可以自定義條件,如下圖:


下面一起看一下運行效果的對比。

 






四、最終的效果展示


 

還是那句話,貼幾張場景的效果圖和使用了屏幕特效后的效果圖。在試玩場景時,除了類似CS/CF的FPS游戲控制系統(tǒng)以外,還可以使用鍵盤上的按鍵【F】,開啟或者屏幕特效。


城鎮(zhèn)一隅(with 屏幕油畫特效):



城鎮(zhèn)一隅(原始圖):



城鎮(zhèn)路口(with 屏幕油畫特效):



城鎮(zhèn)路口(原始圖):



城鎮(zhèn)一隅之二(with 屏幕油畫特效):



城鎮(zhèn)一隅之二(原始圖):



木質(zhì)城墻和手推車(with 屏幕油畫特效):



木質(zhì)城墻和手推車(原始圖):



路邊(with 屏幕油畫特效):



路邊(原始圖):



圖就貼這些,更多畫面大家可以從文章開頭下載的本文配套的exe場景,進行試玩,或者在本文附錄中貼出的下載鏈接中下載本文配套的所有游戲資源的unitypackage。


至此,這篇博文已經(jīng)1萬1千多字。感謝大家的捧場。下周淺墨有些事情,所以停更一次,我們下下周,再會。




附: 本博文相關下載鏈接清單

 

【百度云】博文示例場景exe下載

【百度云】包含博文示例場景所有資源與源碼的unitypackage下載

【Github】屏幕油畫特效實現(xiàn)源碼






    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多