1. 程式人生 > >第14章 非真實感渲染

第14章 非真實感渲染

儘管遊戲渲染一般都是以照相寫實主義( photorealism ) 作為主要目標, 但也有許多遊戲使用了非真實感渲染( Non-Photorealistic Rendering , NPR)的方法來渲染遊戲畫面。非真實感渲染的一個主要目標是, 使用一些渲染方法使得畫面達到和某些特殊的繪畫風格相似的效果,例如卡通、水彩風格等。

在本章中,我們將會介紹兩種常見的非真實感渲染方法。

在14.1 節中,我們將會學習如何實現一個包含了簡單漫反射、高光和描邊的卡通風格的渲染效果。

14.2 節將會介紹一種實時素描效果的實現。

在本章最後,我們還會給出一些關於非真實感渲染的資料,讀者可以在這些文獻中找

到更多非真實感渲染的實現方法。

14.1 卡通風格的渲染

卡通風格是遊戲中常見的一種渲染風格。使用這種風格的遊戲畫面通常有一些共有的特點,例如物體都被黑色的線條描邊,以及分明的明暗變化等。由日本卡普空(英文名: Capcom )株式會社開發的遊戲《大神》(英文名: Okami) 就使用了水墨+卡通風格來渲染整個畫面,如圖14.1所示,這種渲染風格獲得了廣泛讚譽。 要實現卡通渲染有很多方法,其中之一就是使用基於色調的著色技術(tone-based shading )。Gooch 等人在他們1998 年的一篇論文[1]中提出並實現了基於色調的光照模型。在實現中,我們往往會使用漫反射係數對一張一維紋理進行取樣,以控制漫反射的色調。我們曾在7.3 節使用漸變紋理實現過這樣的效果。卡通風格的高光效果也和我們之前學習的光照不同。在卡通風格中,模型的高光往往是一塊塊分界明顯的純色區域。
除了光照模型不同外,卡通風格通常還需要在物體邊緣部分繪製輪廓。在之前的章節中,我們曾介紹使用屏幕後處理技術對螢幕影象進行描邊。在本節,我們將會介紹基於模型的描邊方法,這種方法的實現更加簡單,而且在很多情況下也能得到不錯的效果。
在本節結束後,我們將會實現類似圖14.2 的效果。


14.1.1 渲染輪廓線

在實時渲染中,輪廓線的渲染是應用非常廣泛的一種效果。近20 年來,有許多繪製模型輪廓線的方法被先後提出來。在《Real Time Rendering,third edition》一書中,作者把這些方法分成了 5 種類型。
  • • 基於觀察角度和表面法線的輪廓線渲染。這種方法使用視角方向和表面法線的點乘結果來得到輪廓線的資訊。這種方法簡單快速,可以在一個Pass 中就得到渲染結果,但侷限性很大,很多模型渲染出來的描邊效果都不盡如人意。
  • • 過程式幾何輪廓線渲染。這種方法的核心是使用兩個Pass 渲染。第一個Pass 渲染背面的面片,並使用某些技術讓它的輪廓可見;第二個Pass 再正常渲染正面的面片。這種方法的優點在於快速有效,並且適用於絕大多數表面平滑的模型,但它的缺點是不適合類似於立方體這樣平整的模型。
  • • 基於影象處理的輪廓線渲染。我們在第12 、13 章介紹的邊緣檢測的方法就屬於這個類別。這種方法的優點在於,可以適用於任何種類的模型。但它也有自身的侷限所在,一些深度和法線變化很小的輪廓無法被檢測出來,例如桌子上的紙張。基於輪廓邊檢測的輪廓線渲染。上面提到的各種方法, 一個最大的問題是,無法控制輪廓線的風格渲染。對於一些情況,我們希望可以渲染出獨特風格的輪廓線,例如水墨風格等。
為此,我們希望可以檢測出精確的輪廓邊,然後直接渲染它們。檢測一條邊是否是輪廓邊的公式很簡單,我們只需要檢查和這條邊相鄰的兩個三角面片是否滿足以下條件:

其中, n0 和n1分別表示兩個相鄰三角面片的法向, v 是從視角到該邊上任意頂點的方向。上述公式的本質在於檢查兩個相鄰的三角面片是否一個朝正面、一個朝背面。我們可以在幾何著色器(Geometry Shader)的幫助下實現上面的檢測過程。當然,這種方法也有缺點,除了實現相對複雜外,它還會有動畫連貫性的問題。也就是說,由於是逐幀單獨提取輪廓,所以在幀與幀之間會出現跳躍性。
  • 最後一個種類就是混合了上述的幾種渲染方法。例如,首先找到精確的輪廓邊,把模型和輪廓邊渲染到紋理中,再使用影象處理的方法識別出輪廓線,並在影象空間下進行風格化渲染。
在本節中,我們將會在Unity 中使用過程式幾何輪廓線渲染的方法來對模型進行輪廓描邊。我們將使用兩個Pass 渲染模型:在第一個Pass 中,我們會使用輪廓線顏色渲染整個背面的面片,並在視角空間下把模型頂點沿著法線方向向外擴張一段距離,以此來讓背部輪廓線可見。程式碼如下:
	viewPos = viewPos + viewNormal * _Outline ;
但是,如果直接使用頂點法線進行擴充套件,對於一些內凹的模型,就可能發生背面面片遮擋正面面片的情況。為了儘可能防止出現這樣的情況,在擴張背面頂點之前,我們首先對頂點法線的 z 分量進行處理,使它們等於一個定值,然後把法線歸一化後再對頂點進行擴張。這樣的好處在於,擴充套件後的背面更加扁平化,從而降低了遮擋正面面片的可能性。程式碼如下:
viewNormal.z = -0.5;
viewNormal = normalize(viewNormal);
viewPos = viewPos + viewNormal * _Outline;

14.1.2 新增高光

前面提到過,卡通風格中的高光往往是模型上一塊塊分界明顯的純色區域。為了實現這種效果,我們就不能再使用之前學習的光照模型。回顧一下,在之前實現Blinn-Phong 模型的過程中,我們使用法線點乘光照方向以及視角方向和的一半, 再和另一個引數進行指數操作得到高光反射係數。程式碼如下:
	float spec = pow(max(O, dot(normal, halfDir)), _Gloss)
對於卡通渲染需要的高光反射光照模型,我們同樣需要計算normal 和halfDir 的點乘結果,但不同的是,我們把該值和一個閥值進行比較,如果小於該閥值,則高光反射係數為0,否則返回1 。
	float spec =dot(worldNormal,worldHalfDir);
	spec= step(threshold, spec);
在上面的程式碼中,我們使用CG 的 step 函式來實現和閥值比較的目的。step 函式接受兩個引數,第一個引數是參考值,第二個引數是待比較的數值。如果第二個引數大於等於第一個引數,則返回1 ,否則返回0 。
但是,這種粗暴的判斷方法會在高光區域的邊界造成鋸齒,如圖14.3 左圖所示。 出現這種問題的原因在於,高光區域的邊緣不是平滑漸變的,而是由0 突變到1 。要想對其進行抗鋸齒處理,我們可以在邊界處很小的一塊區域內,進行平滑處理。程式碼如下:
 	float spec = dot(worldNormal, worldHalfDir);
	spec= lerp(O, 1, smoothstep(-w, w, spec - threshold));
在上面的程式碼中,我們沒有像之前一樣直接使用step 函式返回0 或 1 ,而是首先使用了CG的 smoothstep 函式。其中, w 是一個很小的值,當spec - threshold 小於-w 時,返回0,大於w 時,返回1 ,否則在0 到 1 之間進行插值。這樣的效果是,我們可以在 [-w, w]區間內,即高光區域的邊界處,得到一個從0 到1 平滑變化的spec 值,從而實現抗鋸齒的目的。儘管我們可以把w 設為一個很小的定值,但在本例中,我們選擇使用鄰域畫素之間的近似導數值,這可以通過CG 的fwidth 函式來得到。 當然,卡通渲染中的高光往往有更多個性化的需要。例如,很多卡通高光特效希望可以隨意伸縮、方塊化光照區域。Anjyo等人在他們2003 年的一篇論文[2] 中給出了一種風格化的卡通高光的實現。讀者也可以在這篇非真實感渲染的博文 (http://blog.csdn.net/candycat1992/article/details/47284289 )中找到這種方法在Unity 中的實現。

14.1.3 實現

我們現在已經有了理論基礎,是時候在Unity 中驗證我們的結果了。為此,我們需要進行如下準備工作。
( 1) 在Unity 中新建一個場景。在本書資源中,該場景名為Scene_14_1 。在Unity 5.2 中,預設情況下場景將包含一個攝像機和一個平行光,並且使用了內建的天空盒子。在Window → Lighting → Skybox 中去掉場景中的天空盒子。
(2)新建一個材質。在本書資源中,該材質名為ToonShadingMat。
(3 )新建一個Unity Shader. 在本書資源中, 該Unity Shader 名為Chapter14-ToonShading。把新的UnityShader 賦給第2 步中建立的材質。
( 4 )在場景中拖曳一個Suzanne 模型, 並把第2 步中的材質賦給該模型。
( 5 )儲存場景。
開啟Chapter14-ToonShading,關鍵修改如下。
(1)首先,我們需要宣告本例使用各個屬性:
	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Main Tex", 2D) = "white" {}
		_Ramp ("Ramp Texture", 2D) = "white" {}
		_Outline ("Outline", Range(0, 1)) = 0.1
		_OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
		_Specular ("Specular", Color) = (1, 1, 1, 1)
		_SpecularScale ("Specular Scale", Range(0, 0.1)) = 0.01
	}
其中,_Ramp 是用於控制漫反射色調的漸變紋理, _Outline 用於控制輪廓線寬度, _OutlineColor 對應了輪廓線顏色,_Specular 是高光反射顏色,_SpecularScale 用於控制計算高光反射時使用的閥 值。 ( 2)定義渲染輪廓線需要的Pass。前面提到過,這個Pass 只渲染背面的三角面片, 因此, 我們需要設定正確的渲染狀態:
	Pass {
		NAME "OUTLINE"
			
		Cull Front
我們使用Cull 指令把正面的三角面片剔除,而只渲染背面。值得注意的是, 我們還使用NAME 命令為該Pass 定義了名稱。這是因為, 描邊在非真實感渲染中是非常常見的效果, 為該Pass 定義名稱可以讓我們在後面的使用中不需要再重複編寫此Pass,而只需要呼叫它的名字即可。
(3)定義描邊需要的頂點著色器和片元著色器:
			float4 vertex : POSITION;
				float3 normal : NORMAL;
			}; 
			
			struct v2f {
			    float4 pos : SV_POSITION;
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				float4 pos = mul(UNITY_MATRIX_MV, v.vertex); 
				float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);  
				normal.z = -0.5;
				pos = pos + float4(normalize(normal), 0) * _Outline;
				o.pos = mul(UNITY_MATRIX_P, pos);
				
				return o;
			}
			
			float4 frag(v2f i) : SV_Target { 
				return float4(_OutlineColor.rgb, 1);               
			}
如14.1.1 節所講, 在頂點著色器中我們首先把頂點和法線變換到視角空間下,這是為了讓描邊可以在觀察空間達到最好的效果。隨後, 我們設定法線的 z 分量, 對其歸一化後再將頂點沿其方向擴張, 得到擴張後的頂點座標。對法線的處理是為了儘可能避免背面擴張後的頂點擋住正面的面片。最後, 我們把頂點從視角空間變換到裁剪空間。
片元著色器的程式碼非常簡單,我們只需要用輪廓線顏色渲染整個背面即可。
( 4)然後,我們需要定義光照模型所在的 Pass,以渲染模型的正面。由於光照模型需要使用 Unity 提供的光照等資訊, 我們需要為Pass 進行相應的設定,並新增相應的編譯指令:
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			Cull Back
		
			CGPROGRAM
		
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_fwdbase
在上面的程式碼中,我們將LightMode 設定為ForwardBase, 並且使用#pragma 語句設定了編譯指令,這些都是為了讓Shader 中的光照變數可以被正確賦值。
( 5 )隨後,我們定義了頂點著色器:
			struct v2f {
				float4 pos : POSITION;
				float2 uv : TEXCOORD0;
				float3 worldNormal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
				SHADOW_COORDS(3)
			};
			
			v2f vert (a2v v) {
				v2f o;
				
				o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
				o.worldNormal  = UnityObjectToWorldNormal(v.normal);
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				
				TRANSFER_SHADOW(o);
				
				return o;
			}
在上面的程式碼中,我們計算了世界空間下的法線方向和頂點位置,並使用Unity 提供的內建巨集SHADOW_COORDS 和 TRANSFER_SHADOW 來計算陰影所需的各個變數。這些巨集的實現原理可以參見9.4 節。
( 6 )片元著色器中包含了計算光照模型的關鍵程式碼:
			float4 frag(v2f i) : SV_Target { 
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
				fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
				fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
				
				fixed4 c = tex2D (_MainTex, i.uv);
				fixed3 albedo = c.rgb * _Color.rgb;
				
				fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
				
				fixed diff =  dot(worldNormal, worldLightDir);
				diff = (diff * 0.5 + 0.5) * atten;
				
				fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
				
				fixed spec = dot(worldNormal, worldHalfDir);
				fixed w = fwidth(spec) * 2.0;
				fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
				
				return fixed4(ambient + diffuse + specular, 1.0);
			}
首先,我們計算了光照模型中需要的各個方向向量,並對它們進行了歸一化處理。然後,我們計算了材質的反射率albedo 和環境光照ambient 。接著,我們使用內建的UNITY_LIGHT_ATTENUATION 巨集來計算當前世界座標下的陰影值。隨後,我們計算了半蘭伯特漫反射係數,並和陰影值相乘得到最終的漫反射係數。我們使用這個漫反射係數對漸變紋理 _Ramp 進行取樣,並將結果和材質的反射率、光照顏色相乘,作為最後的漫反射光照。高光反射的計算和14.1.2 節中介紹的方法一致,我們使用fwidth 對高光區域的邊界進行抗鋸齒處理,並將計算而得的高光反射係數和高光反射顏色相乘,得到高光反射的光照部分。值得注意的是,我們在最後還使用了 step(0.000 1 , _SpecularScale),這是為了在 _SpecularScale 為0 時,可以完全消除高光反射的光照。
最後,返回環境光照、漫反射光照和高光反射光照疊加的結果。
(7)最後,我們為Shader 設定了合適的Fallback:
	FallBack "Diffuse"
這對產生正確的陰影投射效果很重要(詳見9.4 節)。
本節實現的卡通渲染光照模型是一種非常簡單的實現。在商業專案中,我們往往需要設計和實現更復雜的光照模型,以得到出色的卡通效果。一個很好的例子是遊戲《軍團要塞2》(英文名: Team Fortress 2 )的渲染效果。Valve 公司在2007 年發表了一篇著名的文章[3],解釋了他們在實現該遊戲時使用的相關技術。

14.2 素描風格的渲染

另一個非常流行的非真實感渲染是素描風格的渲染。微軟研究院的Praun 等人在2001 年的SIGGRAPH 上發表了一篇非常著名的論文[4]。在這篇文章中,他們使用了提前生成的素描紋理來實現實時的素描風格渲染,這些紋理組成了一個色調藝術對映( Tonal Art Map, TAM ),如圖14.4所示。在圖14.4 中,從左到右紋理中的筆觸逐漸增多,用於模擬不同光照下的漫反射效果,從上到下則對應了每張紋理的多級漸遠紋理( mipmaps )。這些多級漸遠紋理的生成並不是簡單的對上一層紋理進行降取樣,而是需要保持筆觸之間的間隔,以便更真實地模擬素描效果。 本節將會實現簡化版的論文中提出的演算法,我們不考慮多級漸遠紋理的生成,而直接使用6 張素描紋理進行渲染。在渲染階段,我們首先在頂點著色階段計算逐頂點的光照,根據光照結果來決定6 張紋理的混合權重,並傳遞給片元著色器。然後,在片元著色器中根據這些權重來混合6 張紋理的取樣結果。在學習完本節後,我們會得到類似圖14.5 的效果。 為此,我們需要進行如下準備工作。
(1)在Unity中新建一個場景。在本書資源中,該場最名為Scene_14_2 。在Unity 5.2 中,預設情況下場景將包含一個攝像機和一個平行光, 並且使用了內建的天空盒子。在Window -> Lighting -> Skybox 中去掉場景中的天空盒子。
( 2 )新建一個材質。在本書資源中,該材質名為HatchingMat.
( 3 )新建一個Unity Shader。在本書資源中,該Unity Shader 名為Chapter 14-Hatching。把新的Unity Shader 賦給第2 步中建立的材質。
( 4 )在場景中拖曳一個TeddyBear 模型, 並把第2 步中的材質賦給該模型。為了得到更好的效果,我們還把一張紙張影象拖曳到場景中作為背景。
( 5 ) 儲存場景。
開啟Chapter14-Hatching, 進行如下關鍵修改。
(1)首先,宣告渲染所需的各個屬性:

	Properties {
		_Color ("Color Tint", Color) = (1, 1, 1, 1)
		_TileFactor ("Tile Factor", Float) = 1
		_Outline ("Outline", Range(0, 1)) = 0.1
		_Hatch0 ("Hatch 0", 2D) = "white" {}
		_Hatch1 ("Hatch 1", 2D) = "white" {}
		_Hatch2 ("Hatch 2", 2D) = "white" {}
		_Hatch3 ("Hatch 3", 2D) = "white" {}
		_Hatch4 ("Hatch 4", 2D) = "white" {}
		_Hatch5 ("Hatch 5", 2D) = "white" {}
	}
其中, _Color 是用於控制模型顏色的屬性。_TileFactor 是紋理的平鋪係數,_TileFactor 越大,模型上的素描線條越密,在實現圖14.5 的過程中,我們把 _TileFactor 設定為8。 _Hatch0 至 _Hatch5對應了渲染時使用的6 張素描紋理,它們的線條密度依次增大。
( 2 )由於素描風格往往也需要在物體周圍渲染輪廓線,因此我們直接使用14.1 節中渲染輪廓線的Pass:
	SubShader {
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		UsePass "Unity Shaders Book/Chapter 14/Toon Shading/OUTLINE"
我們使用UsePass 命令呼叫了14.1 節中實現的輪廓線渲染的Pass, Unity Shaders Book/Chapter 14/Toon Shading 對應了14.1 節中Chapter14-ToonShading 檔案裡Shader 的名字,而Unity 內部會把Pass 的名稱全部轉成大寫格式,所以我們需要在UsePass 中使用大寫格式的Pass 名稱。
(3)下面,我們需要定義光照模型所在的Pass。為了能夠正確獲取各個光照變數,我們設定了Pass 的標籤和相關的編譯指令:
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag 
			
			#pragma multi_compile_fwdbase
( 4 ) 由於我們需要在頂點著色器中計算6 張紋理的混合權重,我們首先需要在v2f 結構體中新增相應的變數:
			struct v2f {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				fixed3 hatchWeights0 : TEXCOORD1;
				fixed3 hatchWeights1 : TEXCOORD2;
				float3 worldPos : TEXCOORD3;
				SHADOW_COORDS(4)
			};

 由於一共聲明瞭6 張紋理,這意味著需要6 個混合權重,我們把它們儲存在兩個fixed3 型別的變數(hatchWeights0 和
  
 
  hatchWeights1)中。為了新增陰影效果,我們還聲明瞭worldPos 變數,並使用SHADOW_COORDS 巨集聲明瞭陰影紋理的取樣座標。
( 5 )然後,我們定義了關鍵的頂點著色器:
			v2f vert(a2v v) {
				v2f o;
				
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				
				o.uv = v.texcoord.xy * _TileFactor;
				
				fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
				fixed diff = max(0, dot(worldLightDir, worldNormal));
				
				o.hatchWeights0 = fixed3(0, 0, 0);
				o.hatchWeights1 = fixed3(0, 0, 0);
				
				float hatchFactor = diff * 7.0;
				
				if (hatchFactor > 6.0) {
					// Pure white, do nothing
				} else if (hatchFactor > 5.0) {
					o.hatchWeights0.x = hatchFactor - 5.0;
				} else if (hatchFactor > 4.0) {
					o.hatchWeights0.x = hatchFactor - 4.0;
					o.hatchWeights0.y = 1.0 - o.hatchWeights0.x;
				} else if (hatchFactor > 3.0) {
					o.hatchWeights0.y = hatchFactor - 3.0;
					o.hatchWeights0.z = 1.0 - o.hatchWeights0.y;
				} else if (hatchFactor > 2.0) {
					o.hatchWeights0.z = hatchFactor - 2.0;
					o.hatchWeights1.x = 1.0 - o.hatchWeights0.z;
				} else if (hatchFactor > 1.0) {
					o.hatchWeights1.x = hatchFactor - 1.0;
					o.hatchWeights1.y = 1.0 - o.hatchWeights1.x;
				} else {
					o.hatchWeights1.y = hatchFactor;
					o.hatchWeights1.z = 1.0 - o.hatchWeights1.y;
				}
				
				o.worldPos = mul(_Object2World, v.vertex).xyz;
				
				TRANSFER_SHADOW(o);
				
				return o; 
			}
我們首先對頂點進行了基本的座標變換。然後,使用 _TileFactor 得到了紋理取樣座標。在計算6 張紋理的混合權重之前,我們首先需要計算逐頂點光照。因此,我們使用世界空間下的光照方向和法線方向得到漫反射係數diff。之後, 我們把權重值初始化為0 , 並把diff 縮放到[0, 7]範圍,得到hatchFactor。我們把[0, 7]的區間均勻劃分為7 個子區間,通過判斷hatchFactor 所處的子區間
來計算對應的紋理混合權重。最後,我們計算了頂點的世界座標,並使用 TRANSFER_SHADOW 巨集來計算陰影紋理的取樣座標。 ( 6 )接下來, 定義片元著色器部分:
			fixed4 frag(v2f i) : SV_Target {			
				fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x;
				fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y;
				fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z;
				fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x;
				fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y;
				fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z;
				fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z - 
							i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
				
				fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
				
				UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
								
				return fixed4(hatchColor.rgb * _Color.rgb * atten, 1.0);
			}
當得到了6 六張紋理的混合權重後, 我們對每張紋理進行取樣並和它們對應的權重值相乘得到每張紋理的取樣顏色。我們還計算了純白在渲染中的貢獻度, 這是通過從1 中減去所有6 張紋理的權重來得到的。這是因為素描中往往有留白的部分, 因此我們希望在最後的渲染中光照最亮的部分是純白色的。最後,我們混合了各個顏色值,並和陰影值 atten、模型顏色 _Color 相乘後返回最終的
渲染結果。
(7)最後, 我們設定了合適的Fallback:
FallBack "Diffuse"
讀者也可以生成與本例不同的素描紋理, 具體方法可以參見論文[4]。這篇博文 (https://alastaira.wordpress.com/2013/11/01/hand-drawn-shaders-and-creating-tonal-art-maps/)中還介紹了一種使用Photoshop 等軟體建立相似的素描紋理的方法。

14.3 擴充套件閱讀

在工業界,非真實感渲染己被應用到很多成功的遊戲中, 除了之前提及的《大神》和《軍團要塞2》外, 還有最近的《海島奇兵》《三國志》等遊戲都可以看到非其實感渲染的身影。在學術界, 有更多出色的非真實感渲染的工作被提了出來。讀者可以在國際討論會NPAR( Non-Photorealistic Animation and Rendering )上找到許多關於非真實感渲染的論文。浙江大學的耿衛東教授編篡的書籍《藝術化繪製的圖形學原理與方法》(英文名: The Algorithms and Principles of Non-photorealistic Graphics)[5], 也是非常好的學習材料。這本書概述了近年來非真實感渲染在各個領域的發展,並簡述了許多有重要貢獻的演算法過程, 是一本非常好的參考書籍。
在Unity 的資源商店中, 也有許多優秀的非真實感渲染資源。例如, 
Toon Shader Free  ( https://www.assetstore.unity3d.com/cn/#!/content/21288 )是一個免費的卡通資源包, 裡面實現了包括 輪廓線渲染等卡通風格的渲染。Toon Styles Shader Pack ( https://www.assetstore.unity3d.com/ cn/#!/content/7212 )是一個需要收費的卡通資源包,它包含了更多的卡通風格的Unity Shader 。 Hand-Drawn Shader Pack ( http://www.assetstore.unity3d.com/cn/#!/content/12465 )同樣是一個需 要收費的非真實感渲染效果包, 它包含了諸如鉛筆渲染、蠟筆渲染等多種手繪風格的非真實感渲 染效果。

14.4 參考文獻