這些簡單的shader程序都是寫於2015年的暑假。當時實驗室空調壞了,30多我的在實驗室中揮汗如雨,悶熱中學習shader的日子還歷歷在目。這些文章閒置在我我的博客中,一年將過,師弟也到了學shader的時候,這些例程雖然很簡單,剛接觸shader時卻能夠練練手,因此從我的博客中中搬了出來。而對於有一個月以上shaderLab編程經驗的同窗來講,這篇文章能夠不用看了:-)html
表面着色器只存在於Unity中,算是Unity微創新自創的一套着色器標準。它使得shader的書寫門檻下降,使shader技術更容易使用。表面着色器的一些特性以下:git
SurfaceShader能夠當作是一個光照VS/FS的生成器,它減小了開發者重複編寫代碼的工做。github
SurfacebShader的語句編寫在CGPROGRAM...ENDCG塊內,並且SurfacebShader不容許有pass,它本身會編譯成多個Pass。編程
SurfacebShader使用一個編譯指令來聲明它是一個表面着色器。緩存
表面着色器的三要素是:編譯指令、輸入結構、輸出結構編輯器
#pragma surface surfaceFunction lightModel [optionalparams]ide
這個編譯指令的參數可分爲如下兩類:函數
surfaceFunction:表示Cg函數中有表面着色器(surface shader)代碼。這個函數的格式應該是這樣:void surf (Input IN,inout SurfaceOutput o), Input是你本身定義的結構。Input結構中應該包含全部紋理座標(texture coordinates)和表面函數(surfaceFunction)所須要的額外的必需變量。工具
lightModel :光照模型,內置的光照模型有Lambert與BlinnPhong。咱們也能夠定義本身的光照模型在這裏做爲指令的參數(在後面進行解釋)。關於Lambert與BlinnPhong光照模型能夠參考這篇文章:常見光照模型解析學習
當SurfaceShader編譯指令指定了表面函數surf與一個Lambert漫反射光照模型,這時編譯指令是這樣的:
#pragma surface surf Lambert
這個surf就是表面函數了,表面函數的聲明以下所示:
void surf (Input IN, inout SurfaceOutput o)
能夠看到,surf函數含有兩個參數,第一個是Input類型的IN,Input是什麼類型?實際上,Input是你本身寫定義的輸入結構,這個結構一般擁有着色器須要的全部紋理座標信息,這個紋理座標必須被命名爲「uv」後接紋理名,或者是uv2開始,即便用第二紋理座標集,除了紋理的UV信息,你也能夠在結構中輸入其餘着色函數須要的數據,這些數據包括:
例以下面是一個咱們本身定義的結構:
//輸入結構 struct Input { float2 uv_MainTex;//紋理貼圖 float2 uv_BumpMap;//法線貼圖 float3 viewDir;//觀察方向 };
要書寫Surface Shader,瞭解表面着色器的標準輸出結構必不可少,定義一個表面函數(上面的surf),須要用自定義的輸入結構來輸入相關的UV或數據信息,並在表面函數體內填充輸出結構SrufaceOutput.surfOutput描述的是表面的特性:反射率、法向量、自發光、鏡面反射度、光澤度、透明度。這部分代碼是使用CG或者是HLSL來編寫的。
頂點着色器計算了須要填充輸入什麼,輸出什麼相關的信息,併產生真實的頂點/像素着色器,以及把渲染路徑傳遞到正向或延時渲染路徑。
那麼,這個標準的輸出結構是這樣的:
struct SurfaceOutput { half3 Albedo; //反射率,也就是紋理顏色值(r,g,b) half3 Normal; //法線,法向量(x, y, z) half3 Emission; //自發光顏色值(r, g,b) half Specular; //鏡面反射度 half Gloss; //光澤度 half Alpha; //透明度 };
而這個結構體的成員,會在sruf函數中進行賦值。好比這樣:
//表面着色函數的編寫 void surf (Input IN, inout SurfaceOutput o) { //反射率,也就是紋理顏色值賦爲(0.6, 0.6, 0.6) o.Albedo= 0.6; // 材質表面光澤度0.8 o.Gloss = 0.8; }
下面創建一個簡單的表面着色器,包含了上面所說的表面着色器三要素,能夠看着代碼結合上面的解說進行理解。
Shader "Example/Diffuse Simple" { P SubShader { Tags { "RenderType" = "Opaque" } //表面着色器代碼寫在CGPROGRAM...ENDCG塊中 CGPROGRAM //要素一:編譯指令 #pragma surface surf Lambert //要素二:自定義輸入結構 struct Input { float4 color : COLOR; }; //要素三:標準輸入結構SurfaceOutput void surf (Input IN, inout SurfaceOutput o) { o.Albedo = 1; //把顏色調爲(1,1,1)即白色 } ENDCG } Fallback "Diffuse" }
運行結果是:
前面咱們介紹了表面着色器的特性以及它的三要素,也就是
編譯指令:
#pragma surface surfaceFunction lightModel [opeionalparams]
咱們說surfaceFunction通常是命名爲surf,也能夠換成其餘的函數名,只要和編譯指令中指定的表面函數名對應上就好。對於lightModel(光照模型),Unity內置的光照模型是Lambert(漫反射)和BlinnPhong(鏡面放射)。可是有時候咱們想使用本身的光照函數來實現特殊的效果,那麼咱們須要提供本身的光照函數。具體應該怎麼作呢?
咱們先來看看使用默認光照模型(Lambert)的表面着色器。代碼以下:
Shader "MyShader/Biild_in LightingModle:Lambert" { Properties { //定義一些變量,可在監視面板中看到 _EmissiveColor("Emissive Color",Color)= (1,1,1,1) _AmbientColor("Ambient Color",Color) = (1,1,1,1) _Slider("Slider",Range(1,10))=5 } SubShader { CGPROGRAM //這裏使用了內置光照模型Lambert #pragma surface surf Lambert //Properties中聲明的變量在這裏要從新聲明,以便下面的代碼使用 float4 _EmissiveColor; float4 _AmbientColor; float _Slider; //輸入結構 struct Input { //包含了uv信息 ,注意變量必須以 uv開頭 float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //填充SrufaceOutput結構 float4 c; c = pow((_EmissiveColor+_AmbientColor),_Slider); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
相應的註釋都寫在代碼註釋中了,若是看了上一篇博客的話,這段代碼應該不難理解。這段代碼使用了Unity內置的光照模型Lambert,定義了自發光與環境光屬性,並設置一個滑動條以改變物體顏色。在Unity中查看該段shader效果:
下面,咱們將使用本身寫的光照函數來替換掉Unity內置的光照模型Lambert。假設咱們的光照函數爲BasicDiffuse,則在編譯指令中聲明光照函數名稱:
#pragma surface surf BaseDiffuse
而在定義光照函數時,咱們須要在函數名前面加上Lighting,也便是:
Lighting <光照函數名稱>
因此咱們的BaseDiffuse函數在定義時這樣寫:
inline float4 LightingBasicDiffuse(...)
有三種可供選擇的光照模型函數:
half4 LightingName (SurfaceOutput s, half3 lightDir, half atten){}
這個函數被用於forward rendering(正向渲染),可是不須要考慮view direction(觀察角度)時。
half4 LightingName (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){}
這個函數被用於forward rendering(正向渲染),而且須要考慮view direction(觀察角度)時。
half4 LightingName_PrePass (SurfaceOutput s, half4 light){}
這個函數被用於須要使用defferred rendering(延遲渲染)時。
爲了將上面這段代碼改成使用咱們本身的光照模型函數的表面着色器,咱們須要作的是:
#pragma surface surf BasicDiffuse
2.加入光照函數,寫在CGPROGRAM...ENDCG塊中:
inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); flaot 4 col; col.rgb = s.Albedo *_LightColor0.rgb *(difLight * atten * 2); col.a = s.Alpha ; return col; }
3.保存,進入unity看編譯結果。完整代碼以下:
Shader "MyShader/BasicDiffuse" { Properties { //定義一些變量,可在監視面板中看到 _EmissiveColor("Emissive Color",Color)= (1,1,1,1) _AmbientColor("Ambient Color",Color) = (1,1,1,1) _Slider("Slider",Range(1,10))=5 } SubShader { CGPROGRAM //這裏使用了內置光照模型Lambert #pragma surface surf BasicDiffuse inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float4 col; col.rgb = s.Albedo *_LightColor0.rgb *(difLight * atten * 2); col.a = s.Alpha ; return col; } //Properties中聲明的變量在這裏要從新聲明,以便下面的代碼使用 float4 _EmissiveColor; float4 _AmbientColor; float _Slider; //輸入結構 struct Input { //包含了uv信息 ,注意變量必須以 uv開頭 float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //填充SrufaceOutput結構 float4 c; c = pow((_EmissiveColor+_AmbientColor),_Slider); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
Unity編譯成功後,咱們能夠看看使用默認Lambert光照模型(上圖)和自定義光照模型BasicDiffuse(下圖)的效果圖:
你們對光照函數這段代碼可能還不怎麼理解:
inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); flaot 4 col; col.rgb = s.Albedo *_LightColor0.rgb *(difLight * atten * 2); col.a = s.Alpha ; return col; }
half4 LightingName (SurfaceOutput s, half3 lightDir, half atten){}
這個函數被用於forward rendering(正向渲染),可是不須要考慮view direction(觀察角度),對於漫反射來講,從哪一個角度看到的光照效果都是相同的。
float difLight = max (dot(s.Normal , lightDir), 0 );
使用了max與dot函數,其中dot函數是向量的點乘函數,向量a點乘向量b爲:
a * b = |a| |b| cos< a, b >
因爲dot函數的兩個參數都是單位向量,咱們能夠認爲dot(s.Normal,lightDir)的結果是燈光方向向量與平面某點法向量夾角的餘弦值。因爲餘弦值多是負的,故使用max函數來保證最後獲得的值>=0,避免出現了非預期的效果。
在上文,咱們在表面着色器中定義了本身的光照函數BasicDiffuse,咱們將對這個基本的diffuse進行改造,改形成一種在遊戲《半條命2》中首次使用的光照模型--半Lambert光照,最後咱們將學習使用漸變圖來渲染漫反射。首先貼出咱們上篇文章中寫下來的代碼:
Shader "MyShader/BasicDiffuse" { Properties { //定義一些變量,可在監視面板中看到 _EmissiveColor("Emissive Color",Color)= (1,1,1,1) _AmbientColor("Ambient Color",Color) = (1,1,1,1) _Slider("Slider",Range(1,10))=5 } SubShader { CGPROGRAM //這裏使用了內置光照模型Lambert #pragma surface surf BasicDiffuse inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float4 col; col.rgb = s.Albedo *_LightColor0.rgb *(difLight * atten * 2); col.a = s.Alpha ; return col; } //Properties中聲明的變量在這裏要從新聲明,以便下面的代碼使用 float4 _EmissiveColor; float4 _AmbientColor; float _Slider; //輸入結構 struct Input { //包含了uv信息 ,注意變量必須以 uv開頭 float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //填充SrufaceOutput結構 float4 c; c = pow((_EmissiveColor+_AmbientColor),_Slider); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
若是你看過以前的文章,應該不會對Lmabert光照模型感到陌生,它就是Unity內置的光照模型。Lambert定律認爲在平面某點的漫反射光的光強與該反射點的法向量和入射光角度的餘弦值成正比。Half Lambert 最初是由Value提出來的,用於《半條命2》的畫面渲染,它是爲了防止某個物體背光面丟失而顯得太過平面化。這個光照模型是沒有基於任何物理原理的,它的提出僅僅是一種感性的視覺加強。
咱們先在上面這段代碼中加上幾句代碼及刪除一些代碼,使得物體可使用貼圖進行渲染:
Shader "MyShader/BasicDiffuse" { Properties { //定義一些變量,可在監視面板中看到 //新增 _MainTex("Main Texture",2D) = "white "{} } SubShader { CGPROGRAM //這裏使用了內置光照模型Lambert #pragma surface surf BasicDiffuse inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float4 col; col.rgb = s.Albedo *_LightColor0.rgb *(difLight * atten * 2); col.a = s.Alpha ; return col; } //Properties中聲明的變量在這裏要從新聲明,以便下面的代碼使用 //新增:注意在這裏進行聲明 sampler2D _MainTex; //輸入結構 struct Input { //包含了uv信息 ,注意變量必須以 uv開頭 float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //填充SrufaceOutput結構 float4 c; //新改動:同時要修改這裏 c = tex2D(_MainTex,IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
咱們把場景中方向光調整至使得模型正面處於背光狀態,來看看模型此時的效果:
說了這麼多,咱們仍是要來演示一下Half Lambert的效果,代碼改動上很是簡單,咱們只要稍微修改上面的LightingBasicDiffuse函數:
inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float hLambert = difLight *0.5+0.5; float4 col; col.rgb = s.Albedo *_LightColor0.rgb *(hLambert * atten * 2); col.a = s.Alpha ; return col; }
由代碼能夠看出,咱們定義了一個新的變量hLambert來替換difLight用於計算某點的顏色值。difLight的範圍是0.0-1.0,而經過hLambert,咱們將結果由0.0-1.0映射到了0.5-1.0,從而達到了增長亮度的目的。
保存代碼,Unity編譯好後再看模型,發現模型比剛纔亮度增長了不少:
下面這張圖也一樣展現了Lambert光照與Half Lambert光照的區別:
使用漸變圖來控制漫反射光照的顏色,容許你着重強調surface的顏色,而減弱漫反射光線或其餘光線的影響,這種技術在《軍團要塞2》中流行起來:
這種技術也是由Value提出來的,用來渲染他們的遊戲角色,經常使用於非寫實畫面的,好比在不少卡通風格的遊戲中能夠看到這種技術。
漸變圖可使用PS來製做。製做過程再也不多說。咱們先使用下面這個漸變圖來:
咱們須要新增一張貼圖,方式與_MainTex相同。而後咱們着重改動光照函數的代碼:
inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float hLambert = difLight *0.5+0.5; //新增長 float3 ramp = tex2D (_RampTex,float2(hLambert,hLambert)).rgb; float4 col; //改動 col.rgb = s.Albedo *_LightColor0.rgb *ramp*(atten*2); col.a = s.Alpha ; return col; }
這裏重點代碼是:
float3 ramp = tex2D (_RampTex,float2(hLambert,hLambert)).rgb;
這行代碼返回一個rgb值。tex2D函數接受兩個參數:第一個參數是操做的texture,第二個參數是須要採樣的UV座標。這裏,咱們使用一個漫反射浮點值(即hLambert)來映射到漸變圖上的某一個顏色值。最後獲得的結果即是,咱們將會根據計算獲得的Half Lambert光照值來決定光線照射到一個物體表面的顏色變化。
這裏貼上半Lambert+漸變圖渲染的最終代碼:
Shader "MyShader/HalfLambert_RampTexture" { Properties { //定義一些變量,可在監視面板中看到 _MainTex("Main Texture",2D) = "white "{} _RampTex("RampTexture",2D)="white"{} } SubShader { Tags { "RenderType" = "Opaque" } CGPROGRAM //這裏使用了內置光照模型Lambert #pragma surface surf BasicDiffuse //Properties中聲明的變量在這裏要從新聲明,以便下面的代碼使用 sampler2D _MainTex; sampler2D _RampTex; inline float4 LightingBasicDiffuse (SurfaceOutput s,fixed3 lightDir , fixed atten) { float difLight = max (dot(s.Normal , lightDir), 0 ); float hLambert = difLight *0.5+0.5; float3 ramp = tex2D (_RampTex,float2(hLambert,hLambert)).rgb; float4 col; col.rgb = s.Albedo *_LightColor0.rgb *ramp*(atten*2); col.a = s.Alpha ; return col; } //輸入結構 struct Input { //包含了uv信息 ,注意變量必須以 uv開頭 float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //填充SrufaceOutput結構 float4 c; c = tex2D(_MainTex,IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
在監視面板拉上漸變圖:
這時能夠看到
這小節中,咱們將講解如何使用表面着色器來修改紋理Uv座標以滾動貼圖,而後再介紹sprite sheet實現2D動畫。
首先來看看,爲了實現紋理的uv動畫,咱們須要作什麼:
首先,咱們要在ProPerties模塊中加入兩個控制UV座標變換速度的變量:
Properties { //主紋理貼圖 _MainTexture("Main Texture",2D)="white"{} //兩個控制速度的變量 _xRcrollingSpeed("xRcrollingSpeed",float)=1 _yRcrollingSpeed("yRcrollingSpeed",float)=1 }
不要忘記,上面這些變量在CGPROGRAM...ENDCG模塊中要再聲明一遍,由於咱們後面要訪問它們。
CGPROGRAM #pragma surface surf Lambert sampler2D _MainTexture; float _xRcrollingSpeed; float _yRcrollingSpeed; ...
要記得表面着色器的要素之一:輸入結構的定義
struct Input { float2 uv_MainTexture; };
重點來了,在表面函數中咱們進行座標的變化:
void surf(Input IN,inout SurfaceOutput o) { float2 sourceUv = IN.uv_MainTexture; //關注重點在這裏 float xRcrollingSpeed = _xRcrollingSpeed*_Time.y; float yRcrollingSpeed = _yRcrollingSpeed*_Time.y; sourceUv += float2(xRcrollingSpeed,yRcrollingSpeed); float4 c = tex2D(_MainTexture,sourceUv); o.Albedo = c.rgb; o.Alpha = c.a; }
完整的代碼:
Shader "MyShader/ScrollingUV" { Properties { _MainTexture("Main Texture",2D)="white"{} _xRcrollingSpeed("xRcrollingSpeed",float)=1 _yRcrollingSpeed("yRcrollingSpeed",float)=1 } SubShader { CGPROGRAM #pragma surface surf Lambert struct Input { float2 uv_MainTexture; }; sampler2D _MainTexture; float _xRcrollingSpeed; float _yRcrollingSpeed; void surf(Input IN,inout SurfaceOutput o) { float2 sourceUv = IN.uv_MainTexture; float xRcrollingSpeed = _xRcrollingSpeed*_Time.y; float yRcrollingSpeed = _yRcrollingSpeed*_Time.y; sourceUv += float2(xRcrollingSpeed,yRcrollingSpeed); float4 c = tex2D(_MainTexture,sourceUv); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
把這段代碼保存好,回到Unity的Inspector面板,把下面這張圖賦予材質球,點擊運行就能夠看到動態的紋理效果啦。
原本使用錄像工具魯了一段視頻,再轉化爲gif,結果圖片不清晰,仍是不貼出來了。你們能夠在本身的Unity中試驗。
有時候,咱們獲得的圖片中含有某對象的一系列動做幀,把這些動做按順序逐步播放就能獲得連貫的動畫:
那麼這裏講的就是如何把上面這一張圖製做成2D動畫。實際上在Unity已經有許多插件來完成這些工做,可是爲了更好地瞭解2D動畫的原理,熟悉shader如何改變UV座標達到動畫效果,咱們仍是親手來製做一下。完了完成目標,咱們須要作什麼?
新建一個shader,在編輯器中打開,在Properties添加三個新屬性:
Properties { _MainTexture("Main Texture",2D)="white"{} //添加這三個控制屬性 _TexWidth("Sheet Width",float)=0.0 _CellAmount("Cell Amount",float)=0.0 _SwitchSpeed("Switch Speed",Range(1,10))=5 }
2.國際慣例,在CGPROGRAM...與ENDCG中添加上面屬性的聲明
SubShader { CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; float _TexWidth; float _CellCount; float _SwitchSpeed; ENDCG }
3.輸入結構的就再也不提了
4.而後開始寫咱們的表面函數surf了:
void surf(Input IN,inout SurfaceOutput o) { //將uv座標值保存在變量中 float2 spriteUV= IN.uv_MainTex; //計算每一個動做佔據整張圖的百分比 float cellUVPercentage = 1.0/_CellAmount; //經過系統時間計算偏移量來獲得不一樣的小圖 float timeVal = fmod (_Time.y*_SwitchSpeed,_CellAmount); timeVal = ceil(timeVal); //改變x方向上的偏移量 float xValue = spriteUV.x; xValue += timeVal; xValue *=cellUVPercentage; spriteUV = float2(xValue, spriteUV.y); float4 c = tex2D (_MainTex, spriteUV); o.Albedo = c.rgb; o.Alpha = c.a; }
最後貼上咱們shader的完整代碼:
Shader "MyShader/sprite sheet" { Properties { _MainTex("Base (RGB)",2D)="white"{} //添加這三個控制屬性 _TexWidth("Sheet Width",float)=0.0 _CellAmount("Cell Amount",float)=0.0 _SwitchSpeed("Switch Speed",Range(1,30))=12 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; float _TexWidth; float _CellAmount; float _SwitchSpeed; //輸入結構 struct Input { float2 uv_MainTex; }; //表面函數 void surf(Input IN,inout SurfaceOutput o) { //將uv座標值保存在變量中 float2 spriteUV= IN.uv_MainTex; //計算每一個動做佔據整張圖的百分比 float cellUVPercentage = 1.0/_CellAmount; //經過系統時間計算偏移量來獲得不一樣的小圖 float timeVal = fmod (_Time.y*_SwitchSpeed,_CellAmount); timeVal = ceil(timeVal); //改變x方向上的偏移量 float xValue = spriteUV.x; xValue += timeVal; xValue *=cellUVPercentage; spriteUV = float2(xValue, spriteUV.y); float4 c = tex2D (_MainTex, spriteUV); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
float cellUVPercentage = 1.0/_CellAmount;
爲了每一個時刻只顯示一個小圖,咱們須要對整張圖片進行縮放,這裏計算的就是縮放比例。實例中一張texture共有8個小圖,因此 cellUVPercentage = 1.0/_CellAmount = 1.0/8.0 = 0.125;
float timeVal = fmod (_Time.y*_SwitchSpeed,_CellAmount);
首先來看_Time是什麼。它是內置的shader變量,能夠在內置變量進行查詢
能夠看到_Time記錄了從場景開始運行時的時間計數,它有三個參數,表明了不一樣的時間倍數。如_Time.y就表明三倍時間計數。
fmod函數是對浮點數的求餘計算,它返回x/y的餘數。實例中返回的範圍是在0-8間的小數。爲了獲得整數,使用函數ceil函數向上求整:
timeVal = ceil(timeVal);
下面這部分代碼是最難理解的:
float xValue = spriteUV.x;
02.xValue += timeVal;
03.xValue *= cellUVPercentage;
第一行首先聲明一個新的變量xValue,用於存儲用於圖片採樣的x座標。它首先被初始爲surf函數的輸入參數In的橫座標。類型爲Input的輸入參數In表明輸入的texture的UV座標,範圍爲0到1。第二行向原值加上小圖的整數偏移量,最後爲了只顯示一張小圖,咱們還需將x值乘以小圖所佔百分比cellUVPercentage。
保存代碼以後,點擊Play就能夠看到動畫效果啦,固然圖片仍是靜態的...
關於這些圖片,在google搜索sprite sheet就能找到不少。
16:05 2015/08/11 於工學一號館312
環境映射技術模擬一個物體反射它周圍的環境,它假設一個物體的環境是離物體無限遠的,所以環境可以被編碼到一個稱爲環境貼圖的全方位圖像裏,立方貼圖正是一種全方位圖像。全部近來的圖形處理都支持立方貼圖紋理,立方貼圖不是由一副紋理圖像構成,而是由6副。熟悉天空盒製做的同窗應該對如何由6副圖片造成無縫鏈接的環境有很好的理解。下面這幅圖展現了由6副紋理貼圖構成的環境的立方貼圖:
咱們知道一個2D的紋理能夠經過一個2D紋理座標集來在紋理中查詢顏色值,在以前的文章中咱們也對2D紋理的進行紋理存取:
float4 col = tex2D(_MainTex,In.uv_MainTex);
而對於立方貼圖,咱們採用的是一個表示3D方向向量的三元紋理座標集來存取紋理。這個向量能夠當作是從立方體中心射出的光線,當光線向外的時候它會與立方體貼圖的6個表面之一相交。立方體貼圖紋理存取的結果是在與這6個面相交的點的過濾顏色。在Unity表面着色器中,咱們使用texCUBE來完成立方體貼圖的紋理存取:
float colCube = texCube(_CubeMap,In.worldRefl)
其中_CubeMap是立方體紋理貼圖,在下面咱們會介紹在Unity中如何產生靜態立方紋理貼圖。反而是第二個參數,worldRef1是什麼?這是Input提供給咱們的世界空間中的反射向量,環境貼圖一般是基於世界空間來肯定方向的,所以咱們須要在世界空間中計算反射向量。在CG中,咱們必須把頂點的數據以及法向量變換到世界空間中,而後再進行反射光線的計算。
float3 positionW = mul(modelToWorld,position).xyz;
float3 N = mul((float3x3)modelToWorld,normal);
咱們看一個高度反射物體的時候看到的不是物體自己,而是物體反射它周圍的環境。反射視線是基於初始的視線到達表面上某點以及改點的法向量的。當你使用一個立方貼圖來編碼環境從各個方向上看上去的樣子的時候,渲染反射表面上一點大概只須要爲表面上的那個點計算反射的視線方向,而後咱們就能夠基於反射的視線方向來存取立方貼圖,從而爲表面上的這個點決定環境的顏色。
下面這幅圖顯示的是一個物體以及一張立方貼圖。由於咱們是從2D來看的,因此物體只是是梯形,而立方貼圖用正方形來表示。入射光線從眼睛出發指向物體表面某點,根據該點的表面法向量計算反射光線,由反射光線的方向來對立方貼圖進行紋理存取。
下面這張圖顯示這種狀況的幾何排列:
在CG中提供了函數來進行反射光線的計算:
reflect(I ,N )
這個函數爲入射光線I和表面法向量N返回反射向量。
然而在Unity的表面着色器中,咱們使用簡單這一句就完成了紋理存取的一系列的事情。
float colCube = texCube(_CubeMap,In.worldRefl)
咱們必須學會本身製做靜態的立方體貼圖,由於立方體貼圖(Cubemaps)來源咱們的遊戲場景,網上已有的Cubemaps並不適用在咱們的場景中。下面我將提供一個C#腳本,使用這個腳本可以方便快捷地建立一個Cubemap。最終使用了咱們製做的Cubemap完成的Shader是下面這種效果:
咱們開始製做:
在編輯器中打開該腳本,添加以下using指令:
using UnityEngine; using UnityEditor; using System.Collections
咱們的腳本會在Unity編輯器中建立編輯窗口,因此咱們的GenerateStaticCubemap類要繼承於ScriptableWizard類。這使咱們能夠用一些底層函數來完成目標。
public class GenerateStaticCubemap : ScriptableWizard {
咱們須要一個Cumemap類型變量以及一個位置變量來產生最後的立方體貼圖:
public Transform renderPosition; public Cubemap cubemap;
咱們寫一個函數:OnWizardUpdate(),這個函數在它在嚮導(wizard)第一次彈出或者當GUI被用戶改變時(如拖進去某些對象,輸入某些字符等)時被調用,咱們能夠在這裏檢查用戶已經向嚮導中填入咱們須要的全部的資源。在這裏,若是Cubemap或者它的位置(一個transform)沒有被填充,那麼就設置內置變量isValid爲false,直到拿到全部資源。
void OnWizardUpdate() { helpString = "Select transform to render" + " from and cubemap to render into"; if (renderPosition != null && cubemap != null) { isValid = true; } else { isValid = false; } }
當isValid變量爲true時,嚮導將調用OnWizardCreate()函數。咱們在這函數裏來獲得最終的Cubemap:
void OnWizardCreate() { GameObject go = new GameObject("CubemapCamera"); go.AddComponent<Camera>(); go.transform.position = renderPosition.position; go.transform.rotation = Quaternion.identity; go.GetComponent<Camera>().RenderToCubemap(cubemap); DestroyImmediate(go); }
咱們須要從Unity編輯器打開這個嚮導,因此這裏須要MenuItem關鍵詞:
[MenuItem("CookBook/Render Cubemap")] static void RenderCubemap() { ScriptableWizard.DisplayWizard("Render CubeMap", typeof(GenerateStaticCubemap), "Render!"); }
好啦,這樣cubemap生成器就大功告成,完整代碼以下:
using UnityEngine; using UnityEditor; using System.Collections; public class GenerateStaticCubemap : ScriptableWizard { public Transform renderPosition; public Cubemap cubemap; void OnWizardUpdate() { helpString = "Select transform to render" + " from and cubemap to render into"; if (renderPosition != null && cubemap != null) { isValid = true; } else { isValid = false; } } void OnWizardCreate() { GameObject go = new GameObject("CubemapCamera"); go.AddComponent<Camera>(); go.transform.position = renderPosition.position; go.transform.rotation = Quaternion.identity; go.GetComponent<Camera>().RenderToCubemap(cubemap); DestroyImmediate(go); } [MenuItem("CookBook/Render Cubemap")] static void RenderCubemap() { ScriptableWizard.DisplayWizard("Render CubeMap", typeof(GenerateStaticCubemap), "Render!"); } }
回到咱們的Unity編輯器,在編輯器上方能夠看到:
咱們先建立一個Cubemap文件,命名爲MyCubemape,而後再建立一個球體Sphere:
打開咱們寫的Render CubeMap,把MyCubemap以及Sphere賦予它,這裏Sphere主要是來用提供位置的(也便是上面C#腳本中的Transform變量,最後是用來設置攝像機位置的):
點擊Render!,咱們能夠獲得一個立方紋理貼圖:
有了上面的介紹,咱們的shader代碼就好理解多了,若是你從前面的文章一直看下來的,對錶面着色器三要素有了解的,下面這段代碼基本不用解釋了。這裏貼上代碼:
Shader "MyShader/Using CubeMap" { Properties { _MainTex("Main Texture",2D)="white"{} _MainColor("Diffuse Tint",Color)=(1,1,1,1) _CubeMap("CubeMap",CUBE)=""{} _ReflAmount("Reflection Amount",Range(0.01,1))=0.5 } SubShader { CGPROGRAM #pragma surface surf Lambert sampler2D _Maintex; samplerCUBE _CubeMap; float4 _MainTint; float _ReflAmount; //輸入結構 struct Input { float2 uv_MainTex; float3 worldRefl; }; //表面函數 void surf (Input IN ,inout SurfaceOutput o) { half4 c =tex2D(_Maintex,IN.uv_MainTex)*_MainTint; //這句話是重點 o.Emission = texCUBE(_CubeMap,IN.worldRefl).rgb*_ReflAmount; o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
保存,回到Unity編輯器。建立一個材質並綁定上面這段shader代碼,這裏的CubeMap選擇咱們上面剛剛製做的那個,併爲材質賦予一張基本貼圖:
而後把這材質球賦予咱們的Sphere,能夠看到咱們前面的效果已經出來了:
前面的討論中咱們說起,環境映射假設離物體無限遠,這是由於咱們的立方體紋理存取只取決於世界座標下的反射向量,反射向量只決定了方向,而沒有決定距離,即反射向量方向相同時,位置上的變化不影響表面反射外觀,若是環境中全部東東都離表面足夠遠,那麼這種說法就成立了。
另外,也由於如此,環境映射在平面上表現不好,例如鏡子,在鏡面上反射須要依賴於位置。相反的,環境映射在曲面上表現得很好,例如咱們的球。
00:09 2015/08/12 於工學一號館312
在這小節裏,咱們將介紹如何使用法線貼圖來在一個平面上作出凹凸的效果。寫這些文章不只僅只是展現shader代碼的編寫,我更但願把涉及到的,我學習到的知識都與你們分享。那好,在正式講解Shader代碼以前,咱們先來看看凹凸映射效果以及法向量貼圖的知識。
凹凸映射把由一個紋理提供的物體表面法向量的擾動與每一個片斷的光照相結合,來模擬光照與凹凸表面的相互做用,使得原本須要幾何鑲嵌才呈現得出的凹凸效果在一個平面上也能顯示出來。
使用凹凸映射的緣由:
凹凸映射的好處包括了:
咱們傳統的紋理一般包含RGB或RGBA顏色值,對於RGB紋理,每個像素都由三個份量組成,分別表明了紅色、綠色、藍色,一般這些份量都爲一個無符號字節。
法向量貼圖是凹凸貼圖的一種形式,對於法向量貼圖來講,存儲在紋理元素中的不是顏色值,而是法向量。每一個法向量是一個從表面向外指的方向向量。傳統的RGB紋理格式用來存儲法向量貼圖。與顏色值不一樣的是,顏色是無符號的,而方向向量須要有符號值,除了無符號外,紋理中的顏色值一般被限制在[0,1]的範圍內,而方向向量的取值範圍是[-1,1],爲了能使針對無符號顏色的紋理過濾硬件能正常操做,咱們必需要[-1,1]經過縮放與偏移,將其壓縮至[0,1]內:
colorComponent = 0.5 normalComponent + 0.5
而在過濾硬件處理好後,要把法向量拓展回它們自己的範圍,能夠這樣:
normalComponent = 2 * (colorComponent - 0.5)
總結上面這幾段話:經過使用一個RGB紋理的紅色、綠色、藍色份量來存儲一個法向量的x、y和z份量,並對有符號的值進行範圍壓縮到[0,1]無符號範圍,而後法向量能夠被存儲在一個RGB紋理中。
高度圖紋理對每一個像素的高度進行編碼,而不是對向量進行編碼,所以,高度圖在每一個紋理元素存儲了一個單獨的無符號份量,而不是使用3個份量來存儲一個向量。高度圖由黑色,白色和之間的254種漸變灰度所生成,較暗的部分高度較低,教亮部分高度較高。下面顯示是一張高度圖:
咱們的法線貼圖能夠從高度貼圖中生成,生成規則是:
計算高度圖一個紋理元素對應的法向量,須要對給定的紋理元素、它正上方和右方的紋理元素的高度進行採樣,採樣獲得了三個高度值:給定紋理元素的高度Hg,給定紋理元素正上方紋理元素的高度Ha,給定紋理元素右方紋理元素的高度值Hr。提及來還挺繞口的。獲得這三個值以後,就能夠來構成對應法向量了。由Hg,Ha,Hr能夠獲得兩個差分向量:
flaot3 d1 = (1, 0, Ha - Hg )
flaot3 d2 = (0, 1, Hr - Hg )
咱們的法向量能夠由向量d1 與 d2 作外乘,而後規範化(單位化)獲得。 即:
float3 Normal = normalize ( nod1 X d2 )
把一個高度圖轉換爲一個法向量貼圖是一個徹底自動的過程,而且它一般與範圍壓縮在預處理階段進行。
從高度圖到法線貼圖的轉換,z份量老是正的而且一般或必定爲1。z份量一般被存儲在藍色份量重,而範圍壓縮把z值轉化到[0.5,1]範圍,所以,存儲一個RGB紋理中通過範圍壓縮的法向量貼圖最主要的顏色的藍色:
文章剩下的內容來說訴如何在surface shader中使用法線貼圖製造凹凸效果。
寫咱們的shader代碼:
Shader "MyShader/Normal Texture" { Properties { _MainTex("Main Texture",2D)="white"{} _NormalTex("Normal Texture",2D)=""{} _CubeMap ("Cubemap",CUBE)=""{} _Slider("Slider",Range(0.1,1))=0.5 } SubShader { CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; sampler2D _NormalTex; samplerCUBE _CubeMap; float _Slider; struct Input { float2 uv_MainTex; float2 uv_NormalTex; float3 worldRefl; //關鍵1 INTERNAL_DATA }; inline void surf (Input IN,inout SurfaceOutput o) { half4 MainTexCol = tex2D(_MainTex,IN.uv_MainTex); //關鍵2 float3 normals = UnpackNormal(tex2D(_NormalTex,IN.uv_NormalTex)).rgb; o.Normal = normals; //關鍵3 o.Emission = texCUBE(_CubeMap,WorldReflectionVector(IN,o.Normal)).rgb; o.Albedo = MainTexCol.rgb*_Slider; o.Alpha = MainTexCol.a; } ENDCG } FallBack "Diffuse" }
保存shader代碼,回到Unity編輯器,將各類貼圖以及製做好的Cubemap賦予材質,能夠獲得下面的效果:
對比於沒有使用凹凸貼圖的材質球:
最後,咱們將兩種材質賦予兩個sphere,在scene中進行比較:
這段代碼有三個比較關鍵的地方:
INTERNAL_DATA
UnpackNormal函數,看了上面的法向量的存儲應該知道這函數是幹什麼的,它使法向量從RGB範圍恢復到[-1,1]範圍內。
咱們經過聲明INTERNAL_DATA來訪問修改後的法線信息,而後使用WorldReflectionVector (IN, o.Normal)去查找Cubemap中對應的反射信息。因此在surf函數中查詢立方體貼圖的時候有別於上一篇文章的代碼:
texCUBE(_CubeMap,WorldReflectionVector(IN,o.Normal))
23:00 2015/08/13 於工學一號館312
本節主要介紹布料(Cloth)shader的實現。布料在遊戲中很是常見,主角身上的衣服,房間裏的窗簾等等都是布料構成。布料shader的重點在於如何讓布料的纖維適當分散在整個表面的光照,使它看起來有真實布料的質感,如何讓布料上細小的纖維可以產生邊緣關照效果。爲了實現布料效果,咱們須要先來介紹一點物理學的知識。
你們都知道反射與折射,把反射與折射結合在一塊兒就能夠創造涅菲爾效果。通常而言,當光到達兩種材質的接觸面的時候,一部分光發生反射被表面反射出去,另外一部分光發生折射穿過接觸面,這個現象就稱爲涅菲爾現象。水面上就能夠發生涅菲爾效果:當你垂直向水面時才能夠看到水裏的魚,當遠眺水面(視線與水面夾角很小)每每看到的是水面的反光。
涅菲爾效果爲圖像增長的真實性,它容許你建立物體時展現反射與折射的混合,使得物體看起來更像真實世界的物體。
涅菲爾公式描述了多少光背放射和多少光背折射。然而,量化了涅菲爾效果的涅菲爾公式是很是複雜的。因此咱們每每使用經驗公式---而非真正的涅菲爾公式,來模擬涅菲爾效果。實際上在遊戲引擎中不多使用真正的物理公式精確模擬底層物理,一些技巧每每能夠經過不多的計算來實現很不錯的效果。
既然如此,咱們就給出一個菲涅爾公式的近擬:
refletionCoefficient
= max ( 0, min (1, bias + scale * (1+ dot(I,N ) )^ power ) )
公式中I表示入射向量,N表示表面法向量。當I與N幾乎重合的時候(垂直看水面),反射係數幾乎爲0,表面大部分光被折射。當I與N分開的時候(夾角逐步變小),反射係數應該逐漸增長並最終增長到1.也即反射係數的範圍被限制在[0,1]之間。
這個公式得出的結果refletionCoefficient是一個係數,咱們使用它來對反射與折射作權重分配:
Col = refletionCoefficient反射顏色+ (1-refletionCoefficient)折射顏色
這個例子中會用到細節法線貼圖與細節貼圖,咱們將這兩種法線融合在一塊兒,能夠獲得更高層次的表現。在這裏咱們要學習的技術就是如何把兩個法線貼圖的效果融合。這種技術用來模擬細節層次上的凹凸感,分散整個表面的高光反射。
本節用到的幾張貼圖以下:
注意兩張法線貼圖導入Unity後,須要將它們的類型從Texture轉爲Normal map。這背後發生了什麼,我之後再說。
屬性塊
屬性塊包含的屬性以下,
Properties { //布料主要顏色 _MainTint ("Global Tint", Color) = (1,1,1,1) //布料法線貼圖 _BumpMap ("Normal Map", 2D) = "bump" {} //細節法線貼圖 _DetailBump ("Detail Normal Map", 2D) = "bump" {} //細節貼圖 _DetailTex ("Fabric Weave", 2D) = "white" {} //發生涅菲爾效果時的顏色(遠眺水面看到的水面顏色) _FresnelColor ("Fresnel Color", Color) = (1,1,1,1) //發生菲涅爾效果的強度 _FresnelPower ("Fresnel Power", Range(0, 12)) = 3 _RimPower ("Rim FallOff", Range(0, 12)) = 3 //鏡面高光強度 _SpecIntesity ("Specular Intensiity", Range(0, 1)) = 0.2 _SpecWidth ("Specular Width", Range(0, 1)) = 0.2 }
在CGPROGRAM...ENDCG中說明上面屬性:
SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM //這裏要指定咱們本身的光照函數Velvet #pragma surface surf Velvet #pragma target 3.0 //聲明屬性,以便下面的代碼使用這些屬性 sampler2D _BumpMap; sampler2D _DetailBump; sampler2D _DetailTex; float4 _MainTint; float4 _FresnelColor; float _FresnelPower; float _RimPower; float _SpecIntesity; float _SpecWidth; ... ENDCG }
定義輸入結構Input,咱們有三張貼圖,因此以UV開頭定義三個座標成員:
struct Input { float2 uv_BumpMap; float2 uv_DetailBump; float2 uv_DetailTex; };
接下來是重點了,咱們來寫本身的光照函數。記得光照函數要在光照函數名前面加上固定字段Lighting:
inline fixed4 LightingVelvet (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) { //對各類向量都進行規範化 viewDir = normalize(viewDir); lightDir = normalize(lightDir); half3 halfVec = normalize (lightDir + viewDir); fixed NdotL = max (0, dot (s.Normal, lightDir)); //建立鏡面反射係數 float NdotH = max (0, dot (s.Normal, halfVec)); float spec = pow (NdotH, s.Specular*128.0) * s.Gloss; //建立菲涅爾效果 //不要被這兩句話嚇到 //它們也只是量化菲涅爾公式的一種近擬,相似咱們上面介紹的公式。 float HdotV = pow(1-max(0, dot(halfVec, viewDir)), _FresnelPower); float NdotE = pow(1-max(0, dot(s.Normal, viewDir)), _RimPower); //也可使用咱們上面的公式來建立,效果是同樣的 //float HdotV = max(0, min(1,pow((1+dot(-viewDir,s.Normal)),_FresnelPower))); float finalSpecMask = HdotV+NdotE; //輸出最終的顏色 fixed4 c; c.rgb = (s.Albedo * NdotL * _LightColor0.rgb) + (spec * (finalSpecMask * _FresnelColor)) * (atten * 2); c.a = 1.0; return c; }
在這裏咱們使用的並非上面介紹的菲涅爾公式,而是新的一條公式,然而它們的原理都是相同的:都準守菲涅爾效果。咱們能夠畫圖模擬這條公式,結果就會很明瞭。
表面函數。須要的說明也在註釋中說清楚了。
void surf (Input IN, inout SurfaceOutput o) { //對三張貼圖的取樣 half4 c = tex2D (_DetailTex, IN.uv_DetailTex); //UnpackNormal函數是把壓縮的法向量還原到[-1,1]範圍,具體細節之後會說到。 fixed3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb; fixed3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb; //這裏對兩種法向量進行混合,不過操做也很簡單,即對應元素相加而已。 fixed3 finalNormals = float3(normals.x + detailNormals.x, normals.y + detailNormals.y, normals.z + detailNormals.z); o.Normal = normalize(finalNormals); //對高光強度賦值,範圍0-1 o.Specular = _SpecWidth; o.Gloss = _SpecIntesity; //顏色 o.Albedo = c.rgb * _MainTint; //alpha o.Alpha = c.a; }
Shader "Custom/ClothShader" { Properties { _MainTint ("Global Tint", Color) = (1,1,1,1) _BumpMap ("Normal Map", 2D) = "bump" {} _DetailBump ("Detail Normal Map", 2D) = "bump" {} _DetailTex ("Fabric Weave", 2D) = "white" {} //發生涅菲爾效果時的顏色(遠眺水面看到的水面顏色) _FresnelColor ("Fresnel Color", Color) = (1,1,1,1) //發生菲涅爾效果的強度 _FresnelPower ("Fresnel Power", Range(0, 12)) = 3 _RimPower ("Rim FallOff", Range(0, 12)) = 3 //鏡面高光強度 _SpecIntesity ("Specular Intensiity", Range(0, 1)) = 0.2 _SpecWidth ("Specular Width", Range(0, 1)) = 0.2 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Velvet #pragma target 3.0 sampler2D _BumpMap; sampler2D _DetailBump; sampler2D _DetailTex; float4 _MainTint; float4 _FresnelColor; float _FresnelPower; float _RimPower; float _SpecIntesity; float _SpecWidth; struct Input { float2 uv_BumpMap; float2 uv_DetailBump; float2 uv_DetailTex; }; inline fixed4 LightingVelvet (SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) { //對各類向量都進行規範化 viewDir = normalize(viewDir); lightDir = normalize(lightDir); half3 halfVec = normalize (lightDir + viewDir); fixed NdotL = max (0, dot (s.Normal, lightDir)); //建立鏡面反射係數 float NdotH = max (0, dot (s.Normal, halfVec)); float spec = pow (NdotH, s.Specular*128.0) * s.Gloss; //建立菲涅爾效果 //不要被這兩句話嚇到 //它們也只是量化菲涅爾公式的一種近擬,相似咱們上面介紹的公式。 float HdotV = pow(1-max(0, dot(halfVec, viewDir)), _FresnelPower); float NdotE = pow(1-max(0, dot(s.Normal, viewDir)), _RimPower); //也可使用咱們上面的公式來建立,效果是同樣的 //float HdotV = max(0, min(1,pow((1+dot(-viewDir,s.Normal)),_FresnelPower))); float finalSpecMask = HdotV+NdotE; //輸出最終的顏色 fixed4 c; c.rgb = (s.Albedo * NdotL * _LightColor0.rgb) + (spec * (finalSpecMask * _FresnelColor)) * (atten * 2); c.a = 1.0; return c; } void surf (Input IN, inout SurfaceOutput o) { //對三張貼圖的取樣 half4 c = tex2D (_DetailTex, IN.uv_DetailTex); //UnpackNormal函數是把壓縮的法向量還原到[-1,1]範圍,具體細節之後會說到。 fixed3 normals = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)).rgb; fixed3 detailNormals = UnpackNormal(tex2D(_DetailBump, IN.uv_DetailBump)).rgb; //這裏對兩種法向量進行混合,不過操做也很簡單,即對應元素相加而已。 fixed3 finalNormals = float3(normals.x + detailNormals.x, normals.y + detailNormals.y, normals.z + detailNormals.z); o.Normal = normalize(finalNormals); //對高光強度賦值,範圍0-1 o.Specular = _SpecWidth; o.Gloss = _SpecIntesity; //顏色 o.Albedo = c.rgb * _MainTint; //alpha o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
對3張貼圖正確拖拽到材質球上,能夠看到:
將材質球賦給衣服模型:
從一個和衣服表面很小的夾角看,觀察菲涅爾效果:
其餘的效果就本身試試啦。
該Unity工程能夠從這裏下載
10:53 2015/08/14 於工學一號館312
咱們可使用Tags告訴渲染引擎場景中的對象應該何時繪製以及如何來渲染。本篇文章主要來介紹在SubShader中使用的渲染隊列標籤Queue Tags。
Queue Tags 能夠決定一個物體何時被繪製,以爲場景中不一樣標籤的物體的繪製順序,具體使用方法與細節請繼續往下看。
Tags{"TagName1"="Value1" "TagName2"="Value2"}
Tags的數量沒有限制,咱們能夠定義任意多的Tag。
Tags是標準的鍵值對,也就是能夠根據一個鍵值(key)來獲取實值(value)。
SubShader中的Tags被用來決定渲染順序,固然也有其餘做用的標籤,具體能夠看ShaderLab: SubShader Tags。
注意Queue Tags必須寫在subshader中,而不是在pass中。
除了unity提供的預約義tags外,咱們也能夠定義本身的隊列標籤。
使用Queue tag 可以決定咱們的對象以什麼順序被渲染。着色器決定對象屬於哪個渲染隊列,經過這種方法,透明的物體可以被保證在全部不透明物體繪製完後再繪製。
有四種預約義好的Queue tag。以下所示:
Background 渲染隊列值:1000
這個標籤爲背景標籤。這個標籤將在全部其餘標籤以前被渲染,能夠被用來標記做爲背景的對象。
Geometry(默認) 渲染隊列值:2000
這個標籤被最多的對象所使用,不透明的幾何體使用這標籤。
AlphaTest 渲染隊列值:2450
須要Alpha測試的幾何體使用這標籤。它和Geometry隊列不一樣,對於在全部立體物體繪製後渲染的通道檢查的對象,它更有效。
Transparent 渲染隊列值:3000
這個標籤將在Geometry與AlphaTest以後進行渲染。任何通道混合的(也就是說,那些不寫入深度緩存的Shaders)對象使用該隊列,例如玻璃和粒子效果。
Overlay 渲染隊列值:4000
最後須要渲染的對象選擇這個標籤,例如覆蓋物效果、鏡頭光暈等。
在同一個標籤內,咱們也能夠以爲物體的繪製順序,好比
Tags{"Queue"="Geometry-1"} 與 Tags{"Queue"="Geometry"}
前者比後者優先渲染。被渲染隊列值越小的標籤標記的對象越優先渲染。
咱們舉個例子來講明Queue Tags的使用。首先咱們建立一個場景,在場景中擺上兩個球,命名爲ball1與ball2,ball1在ball2前面(離攝像機比較近)。它們的位置關係是這樣的:
接下來咱們寫兩個shader,分別應用在兩個球體上。ball1咱們使用這個shader:
Shader "MyShader/QueueTags" { Properties { _Emissive("Emissive",Color)=(1,1,1,1) _MainTex("Main Texure",2D)=""{} } SubShader { Tags{"Queue"= "Geometry-1"} ZWrite Off CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; float4 _Emissive; struct Input { float2 uv_MainTex; }; inline void surf(Input IN,inout SurfaceOutput o) { float4 col = tex2D(_MainTex,IN.uv_MainTex); o.Albedo = col.rgb+_Emissive; o.Alpha = col.a; } ENDCG } FallBack "Diffuse" }
而ball2使用這個shader:
Shader "MyShader/QueueTags" { Properties { _Emissive("Emissive",Color)=(1,1,1,1) _MainTex("Main Texure",2D)=""{} } SubShader { Tags{"Queue"= "Geometry"} ZWrite Off CGPROGRAM #pragma surface surf Lambert sampler2D _MainTex; float4 _Emissive; struct Input { float2 uv_MainTex; }; inline void surf(Input IN,inout SurfaceOutput o) { float4 col = tex2D(_MainTex,IN.uv_MainTex); o.Albedo = col.rgb+_Emissive; o.Alpha = col.a; } ENDCG } FallBack "Diffuse" }
注意到它們惟一的區別就在於Tags{"Queue"= "Geometry"}與Tags{"Queue"= "Geometry+1"}上。
分別將這兩個shader賦給倆Materal,再將Material拖拽給兩個球體。順便給倆球體調好對比顏色。咱們能夠來看看效果:
能夠看到,原本在ball2(黃色)前面的ball1(紅色)已經跑到ball2的後面去了。
細心的同窗已經發現了,在ball1的shader中有一個語句:
ZWrite Off
這句話的意思是關閉寫入深度緩存,即不把ball1的深刻值寫入深度緩存中,深刻緩存的寫入以及深度測試纔是決定物體是否遮擋的決定性因素。也便是說,就算是ball1先被畫出來,但在進行深度寫入以及深刻測試時,ball1仍是離攝像機比較近,因此黃球被紅球遮擋住的部分就不繪製了,地面被紅球擋住的部分也不繪製了。下面的效果展現了進行深刻緩存寫入的效果(即去掉了ZWrite off):
項目工程能夠在這裏下載。
17:47 2015/08/15 於工學一號館312
在這節咱們使用表面着色器來作卡通效果。卡通效果有許多種表現方法,這能夠寫成一個系列。不過目前我只學習了一種( ̄﹏ ̄)就是今天要講的就是這一種。要表現這種卡通效果要抓住三個point:
咱們先來看一下效果如何:
其中左邊的機器人爲卡通風格,而右邊機器人爲原來的模型。
下面咱們分點來進行卡通風格製做的介紹。
簡化顏色的意思即簡化了模型上使用的顏色。咱們先在Properties添加新屬性:
_SimFac("Simplify Factor",Range(0.1,20))=0.5
相應的,爲了CGPROGRAM ...ENDCG模塊中可使用上面這屬性,咱們要添加它對應的引用:
float _SimFac;
而後在咱們的表面函數surf中添加以下語句來對像素的顏色作簡化:
o.Albedo = floor (o.Albedo*_SimFac)/_SimFac;
咱們定義了 **_SimFac來控制顏色簡化的程度。那麼如何來控制呢?關鍵就在第三行代碼上。floor**函數對操做數進行向下取整,咱們將像素的顏色乘以簡化因子,取整以後再除以簡化因子來達到收縮顏色的效果。
舉個例子:咱們假設顏色爲(0.75,0.75,0.75),簡化因子咱們取2,那麼執行了代碼以後顏色縮爲:(0.5,0.5,0.5)
(0.5,0.5,0.5)= floor((0.75,0.75,0.75)*2)/2
對於0.51~0.99範圍內的顏色,在簡化因子爲2的狀況下,都會被縮爲0.5,從而達到了簡化顏色的目的。
咱們也不難推導出,簡化有因子越大,顏色簡化越弱,例如在簡化因子爲8時,機器人顏色明顯豐富多了:
在上面中有介紹到,咱們使用漸變圖來控制漫反射光照的顏色,容許你着重強調surface的顏色,而減弱反射光線的影響。這種技術在《軍團要塞2》中流行起來,用於渲染非寫實畫面如卡通風格遊戲。在這裏咱們就要把這種技術應用上啦,不瞭解的同窗請看完上面這篇文章。
好,首先咱們須要一張ramp Texture,漸變圖:
這張漸變圖有個特定,按就是邊界明顯,不像咱們之前用過的漸變圖那樣緩慢變化。這是由於卡通風格里常常有分界明顯的明暗變化。
爲了使用這張漸變圖,咱們在Properties添加新屬性:
_RampTex("Ramp Texture",2D)=""{}
一樣的,在CGPROGRAM ...ENDCG模塊中引用它:
sampler2D _RampTex;
而後,在咱們自定義的光照函數Cartoon中添加以下代碼
float NdotL = max(0,dot(s.Normal,lightDir)); float hNdotL = NdotL *0.5+0.5; float NdotV = max(0,dot(s.Normal,viewDir)); float hNdotV = NdotV*0.5+0.5; float3 ram= tex2D(_RampTex,float2(hNdotL,hNdotV)).rgb;
看了Diffuse Shading——漫反射光照改善技巧相信可以理解上面這段代碼,也就是更具幾個向量來計算漸變圖取樣座標。
咱們來看看效果:
沒有使用漸變圖:
使用了漸變圖:
首先咱們要先解決一個問題,怎麼判斷一個像素點位於模型的邊緣(輪廓)?沒錯,就是使用向量的點乘來判斷,對於一個球體來講,球體邊緣的法向量與從正面看的觀察向量成90度角,這兩個向量的點乘結果爲0.咱們可使用一個閾值來控制邊緣的大小。具體代碼請看:
首先咱們仍是定義一個邊緣閾值:
_OutLine("OutLine",Range(0,1))=0.1
一樣的,在下面的模塊中對它進行引用:
float _OutLine;
而後重點代碼就來了,在表面函數surf中,咱們添加以下代碼:
//對觀察向量與表面法向量進行點乘 float OutLine = max(0,dot(normalize( o.Normal) ,normalize( IN.viewDir))); //C語言的語法。根據閾值進行描邊。 OutLine = OutLine< _OutLine ? OutLine / 4 : 1; o.Albedo = MainCol.rgb * _Emissive.rgb * OutLine;
主要來看第二行代碼,當兩個法向量的點乘小於閾值時,咱們把表面像素點的最終顏色降爲原來的1/4,造成黑色的效果,不然則保持原來的顏色(乘以1)。
來看看效果吧,咱們來看機器人的腦袋能夠看到明顯的黑邊:
把這三個點get到以後,咱們就能夠作出咱們的卡通效果啦!~
Shader "MyShader/Cartoon1" { Properties { _RampTex("Ramp Texture",2D)=""{} _SimFac("Simplify Factor",Range(0.1,20))=0.5 _OutLine("OutLine",Range(0,1))=0.1 _MainTex("Main Texture",2D)=""{} _Emissive("Emissive",Color)=(1,1,1,1) _BumpTex("Bump Texture",2D)=""{} } SubShader { CGPROGRAM #pragma surface surf Cartoon sampler2D _RampTex; sampler2D _MainTex; sampler2D _BumpTex; float _SimFac; float _OutLine; float4 _Emissive; struct Input { float2 uv_MainTex; float2 uv_BumpTex; float3 viewDir; }; inline void surf(Input IN,inout SurfaceOutput o) { float4 MainCol = tex2D(_MainTex,IN.uv_MainTex); o.Normal = UnpackNormal( tex2D(_BumpTex, IN.uv_BumpTex)); float OutLine = max(0,dot(normalize( o.Normal) ,normalize( IN.viewDir))); //對周圍黑邊進行處理 OutLine = OutLine< _OutLine ? OutLine / 4 : 1; o.Albedo = MainCol.rgb * _Emissive.rgb * OutLine; //對顏色進行簡化 o.Albedo = floor (o.Albedo*_SimFac)/_SimFac; o.Alpha = _Emissive.a; } inline float4 LightingCartoon(SurfaceOutput s,float3 viewDir,float3 lightDir,float atten) { float NdotL = max(0,dot(s.Normal,lightDir)); float hNdotL = NdotL *0.5+0.5; float NdotV = max(0,dot(s.Normal,viewDir)); float hNdotV = NdotV*0.5+0.5; float3 ram= tex2D(_RampTex,float2(hNdotL,hNdotV)).rgb; float4 col; col.rgb = s.Albedo* ram*_LightColor0.rgb ; col.a = s.Alpha; return col; } ENDCG } }
這種卡通描邊方式是有缺陷的,在較平的平面上會出現突變產生整片的黑色區域:
這是由於咱們採用了頂點法向量來判斷邊界的,那麼對於正方體這種法線固定單一的狀況,判斷出來的邊界要麼基本不存在要麼就大的離譜!對於這樣的對象,一個更好的方法是用Pixel&Fragment Shader、通過兩個Pass渲染描邊:第一個Pass,咱們只渲染背面的網格,在它們的周圍進行描邊;第二個Pass中,再正常渲染正面的網格。其實,這是符合咱們對於邊界的認知的,咱們看見的物體也都是看到了它們的正面而已。
不過,問題老是會有解決辦法的!在之後的卡通shader中咱們會解決這個問題的o(^▽^)o