其實這是現(xiàn)在一些游戲 很喜歡的渲染方式块请,最后出來的效果還是很不錯的
非真實感渲染 (Non-Photorealistic Rendering, NPR)的方法來渲染游戲畫面晕讲。得到類似水彩覆获,卡通的風格马澈,其實可以模擬中國水墨畫感覺
卡通風格的渲染
卡通風格,有一些共有的特點弄息,物體都被黑色的線條描邊痊班,以及分明的明暗變化。實現(xiàn)卡通渲染有很多方法疑枯,一種就是使用基于色調(diào)的著色技術(shù)(tone-based shading)辩块,Gooch等人也在一篇論文中提出并實現(xiàn)了基于色調(diào)的光照模型蛔六。在實現(xiàn)中荆永,我們往往會使用漫反射系數(shù)對一張一維紋理進行采樣,以控制漫反射的色調(diào)国章【咴浚卡通風格的高光效果也和我們之前學習的光照不同,卡通風格中液兽,模型的高光往往是一塊塊分界明顯的純色區(qū)域骂删。除了光照模型不同外,卡通風格通常還需要在物體邊緣部分繪制輪廓四啰。使用屏幕后處理技術(shù)對圖像進行描邊宁玫。這里我們將基于模型的描邊方式,這種方法實現(xiàn)更加簡單柑晒,也能得到不錯的效果欧瘪。
渲染輪廓線
在實時渲染中,輪廓線渲染是應(yīng)用非常廣泛的一種效果匙赞。在《Real Time Rendering, third edition》一書中佛掖,作者分成了5種類型
- 基于觀察角度和表面法線的輪廓線渲染。這種方法使用視角方向和表面法線的點乘結(jié)果來得到輪廓線的信息涌庭。這種方法簡單快速芥被,可以在一個Pass中得到渲染結(jié)果,但局限性很大坐榆,很多模型渲染出來的效果不盡如人意拴魄。
- 過程式幾何輪廓線渲染,這種方法核心是使用兩個Pass渲染席镀,第一個pass渲染背面的面片匹中,使用某些技術(shù)讓他的輪廓可見;第二個Pass再正常渲染正面的面片愉昆。這種方法的優(yōu)點在于快速有效职员,并且適用于大多數(shù)表面平滑的模型,但他的缺點就是不適合類似立方體的這樣平整的模型跛溉。
- 基于圖像處理的輪廓線渲染焊切,前兩章說的邊緣檢測就是這種方法扮授。優(yōu)點在于適用任何種類的模型,但也有自身的局限性专肪,一些深度和法線變化很小的輪廓無法被檢測出來刹勃,例如桌子上的紙張『坑龋基于輪廓邊檢測的輪廓線渲染荔仁。
各種方法,一個最大的問題就是芽死,無法控制輪廓線的風格渲染乏梁,對于一些情況,我們希望可以渲染出獨特風格的輪廓線关贵,例如水墨風格等遇骑。
為此,我們希望是可以檢測出精確的輪廓邊揖曾,直接渲染他們落萎。檢測一條邊是否是輪廓邊的公式很簡單,只需要檢查和這條邊相鄰的兩個三角面片是否滿足以下條件:
其中炭剪,n0和n1分別表示兩個相鄰三角面片的法向练链, V是從視角到該邊上任意頂點的方法,上述公式的本質(zhì)在于檢查兩個相鄰的三角面片是否一個朝正面奴拦、一個朝背面媒鼓。我們可以在幾何著色器(Geometry Shader)的幫助下實現(xiàn)上面的檢測過程。當然粱坤,這種方法也有缺點隶糕,除了實現(xiàn)相對復雜外,它還會有動畫連貫性的問題站玄。也就是說枚驻,由于是逐幀單獨提取輪廓,所以在幀與幀之間會出現(xiàn)跳躍性株旷。
- 最后一個種類就是混合了上述的幾種渲染方法再登。例如,首先找到精確的輪廓邊晾剖,把模型和輪廓邊渲染到紋理中锉矢,再使用圖像處理的方法識別出輪廓線,并在圖像空間下進行風格化渲染
將會在Unity中 使用過程式幾何輪廓線渲染的方法來對模型進行輪廓描邊齿尽。我們將使用兩個Pass渲染模型:在第一個Pass中沽损,我會使用輪廓線顏色渲染整個背面的面片,并在視角空間下把模型頂點沿著法線方向向外擴張一段距離循头,以此來讓背部輪廓線可見绵估。
viewPos = viewPos + viewNormal *_Outline
但是炎疆,如果直接使用頂點法線進行擴展,對于一些內(nèi)凹的模型国裳,就可能發(fā)生背面面片遮擋正面面片的情況形入,為了盡可能防止這樣的情況,在擴張背面頂點之前缝左,首先對頂點法線的z分量處理亿遂,使它們等于一個定值,然后把法線歸一化后再對頂點進行擴張渺杉。這樣的好處在于蛇数,擴展后的背面更加扁平化,從而降低了遮擋正面面片的可能性
viewNormal.z = -0.5;
viewNormal = normalize(viewNormal);
viewPos = viewPos + viewNormal * _Outline;
添加高光
卡通風格中的高光往往是模型上一塊塊分界明顯的純色區(qū)域少办,為了這種效果苞慢,不能用之前的光照模型。之前實現(xiàn)的Blinn-Phong模型的過程中英妓,我們用法線點乘光照方向以及視角方向和的一半,在和另外一個參數(shù)進行指數(shù)操作得到高光反射系數(shù)绍赛。
float spec = pow(max(0, dot(normal, halfDir)), _Gloss)
對于卡通渲染需要的高光反射光照模型蔓纠,我們同樣需要計算normal和halfDir 的點乘結(jié)果,不同的是吗蚌,把該值和一個閥值進行比較腿倚,如果小于該閥值,則高光反射系數(shù)為0蚯妇,否則返回1
float spec = dot(worldNormal敷燎,worldHalfDir);
spec = step(threshold箩言, spec)硬贯;
我們用CG的step函數(shù) 來實現(xiàn)和閥值比較的目的,step函數(shù)接受兩個參數(shù)陨收,第一個參數(shù)是參考值饭豹,第二個參數(shù)是待比較的值。spec大于threshold 就返回1务漩,否則返回0拄衰。
但是這樣會造成高光區(qū)域的邊界造成鋸齒。
出現(xiàn)這種問題的原因在于饵骨,高光區(qū)域的邊緣不是平滑漸變的翘悉,而是從0突變到1.要想對其進行抗鋸齒處理,可以在邊緣很小的一塊區(qū)域內(nèi)居触,進行平滑處理
float spec = dot(worldNormal, worldHalfDir);
spec = lerp(0,1,smoothstep(-w,w, spec- threshold));
這里使用了** CG的 smoothstep函數(shù)** 其中 w是一個很小的值妖混,當spec- threshold 小于-w時包吝,返回0,大于w時源葫,返回1诗越,否則就在 0和1 之間進行插值。這樣的效果息堂,我們可以在【-w 嚷狞, w】區(qū)間內(nèi),即高光區(qū)域的邊界處荣堰,得到一個從0 到1 的平滑變化的spec值床未,從而實現(xiàn)抗鋸齒效果。這里我們可以把w設(shè)置為一個很小的定值振坚,但這里我們使用領(lǐng)域像素之間的近似導數(shù)值薇搁,可以通過 CG的fwidth函數(shù)來得到。
當然渡八,卡通渲染中的高光往往有更多個性化的需要啃洋。例如,很多卡通高光特效希望可以隨意伸縮屎鳍、方塊化光照區(qū)域宏娄。Anjyo等人在他們2003 年的一篇論文[2] 中給出了一種風格化的卡通高光的實現(xiàn)。讀者也可以在這篇非真實感渲染的博文
(http://blog.csdn.net/candycat1992/article/details/47284289 )中找到這種方法在Unity 中的實現(xiàn)逮壁。
實現(xiàn)
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 是用于控制漫反射色調(diào)的漸變紋理, _Outline 用于控制輪廓線寬度, _OutlineColor對應(yīng)了輪廓線顏色,_Specular 是高光反射顏色窥淆,_SpecularScale 用于控制計算高光反射時使用的閥值卖宠。
2.定義渲染輪廓線的Pass,這個pass只渲染背面的三角米面片忧饭,需要設(shè)置正確的渲染狀態(tài)
Pass{
NAME "OUTLINE"
Cull Front
}
Cull 指令把正面的三角面片剔除扛伍,而只渲染背面。用**NAME命令為該Pass定義名稱眷昆。這樣可以在別的Pass中直接調(diào)用蜒秤,不需要重復寫
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);
}
首先把頂點和法線變換到視角空間下,是為了讓描邊可以在觀察空間達到最好效果亚斋。隨后作媚,設(shè)置法線的z分量,對其歸一化后 再將頂點沿其方向擴張帅刊,得到擴張后的頂點坐標纸泡。對法線的處理是為了盡可能的避免背面擴展后頂點擋住正面的面片。最后赖瞒,把頂點從視角空間變換到裁剪空間女揭。
片元的代碼簡單蚤假,用輪廓線顏色渲染整個背面即可
4.定義 光照模型所在的Pass,以渲染模型正面吧兔。由于需要使用Unity提供的光照等信息磷仰。添加對應(yīng)的編譯命令
Pass{
Tags{"LightMode" = "ForwardBase"}
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
}
將LightMode 設(shè)置為ForwardBase, 并且使用#pragma 語句設(shè)置了編譯指令境蔼,這些都是為了讓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 提供的內(nèi)置宏SHADOW_COORDS 和
TRANSFER_SHADOW 來計算陰影所需的各個變量箍土。這些宏的實現(xiàn)原理可以參見9.4 節(jié)逢享。
6.片元著色器 包含計算 高照模型的關(guān)鍵代碼
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);
}
計算光照模型中需要的各個方向矢量,對他們進行歸一化處理吴藻。 然后瞒爬,計算材質(zhì)的反射率albedo和環(huán)境光照 ambient。
使用內(nèi)置的UNITY_LIGHT_ATTENUATION 宏來計算當前世界坐標下的陰影值沟堡。隨后 計算了半蘭伯特漫反射系數(shù)侧但,并和陰影值相乘得到最終的漫反射系數(shù)。
使用這個漫反射系數(shù)對漸變紋理 _Ramp 進行采樣弦叶,并將結(jié)果和材質(zhì)的反射率俊犯,光照顏色相乘,作為最后的漫反射光照伤哺。
高光反射的計算和上面介紹的技術(shù)一樣。使用fwidth對高光區(qū)域的邊界進行抗鋸齒處理者祖,并將計算而得的高光反射系數(shù)和高光反射顏色相乘立莉,得到高光反射的光照部分。
我們最后還使用了step(0.000 1 七问, _SpecularScale)蜓耻,這是為了在 _SpecularScale 為0 時,可以完全消除高光反射的光照械巡。
最后刹淌,返回環(huán)境光照、漫反射光照和高光反射光照疊加的結(jié)果
- Fallback 使用 FallBack "Diffuse"
素描風格的渲染
微軟研究院的Praun 等人在2001 年的SIGGRAPH 上發(fā)表了一篇非常著名的論文[4]讥耗。在這篇文章中有勾,他們使用了提前生成的素描紋理來實現(xiàn)實時的素描風格渲染,這些紋理組成了一個色調(diào)藝術(shù)映射( Tonal Art Map, TAM )古程,如圖蔼卡,從左到右紋理中的筆觸逐漸增多,用于模擬不同光照下的漫反射效果挣磨,從上到下則對應(yīng)了每張紋理的多級漸遠紋理(mipmaps)這些多級漸遠紋理的生成不是簡單的對上一層紋理進行降采樣雇逞,而是需要保持筆觸之間的間隔荤懂,以便更真實的模擬素描效果。
我們實現(xiàn)簡化版的算法塘砸,不考慮多級漸遠紋理的生成节仿,直接使用6張素描紋理進行渲染掉蔬。在渲染階段廊宪。我們首先在頂點著色器階段計算逐頂點的光照,根據(jù)光照結(jié)果來決定6張紋理的混合權(quán)重眉踱,并傳遞給片元著色器挤忙。然后在片元著色器中根據(jù)權(quán)重來混合6張采樣紋理,
(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 是紋理的平鋪系數(shù)婿禽, _TileFactor 越大赏僧,模型上的素描線條越密,我們把 _TileFactor 設(shè)置為8扭倾。 _Hatch0 至 _Hatch5對應(yīng)了渲染時使用的6 張素描紋理淀零,它們的線條密度依次增大。
(2)素描風格 需要在物體周圍渲染輪廓線膛壹,直接使用 上一節(jié)中的 渲染輪廓線Pass
SubShader{
Tags{"RenderType" = " Opaque" "Queue" = "Geometry"}
UsePass "Unity Shaders Book/Chapter 14/Toon Shading/OUTLINE"
}
這里注意就是shader的名稱驾中, pass名字要全部大寫,原保存的時候Unity會自動轉(zhuǎn)大寫模聋。
(3)定義光照模型的Pass肩民,為了能夠正確獲取各個光照變量,所以要對應(yīng)的編譯指令
Pass{
Tags{ "LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
}
(4)因為有6個紋理的 混合權(quán)重链方,需要在v2f結(jié)構(gòu)中定義相應(yīng)的變量
struct v2f{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed3 hatchWeights0 :TEXCOORD1持痰;
fixed3 hatchWeights1 : TEXCOORD2;
float3 wordlPos : TEXCOORD3;
SHADOW_COORDS(4)
}
由于一共聲明了6 張紋理,這意味著需要6 個混合權(quán)重祟蚀,我們把它們存儲在兩個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 純白 不做事情
}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.hatchWeights0.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張紋理的混合權(quán)重之前,我們首先需要計算逐頂點光照。所以攻人,使用世界空間下的光照方向和法線方向得到漫反射系數(shù)diff取试。
然后把權(quán)重值 初始為0;把diff 縮放到【0,7】范圍怀吻,得到hatchFactor瞬浓。 把【0,7】區(qū)間均勻劃分為7個子區(qū)間,通過判斷hatchFactor所處的區(qū)間來計算對應(yīng)的紋理混合權(quán)重蓬坡。最后猿棉,計算頂點的世界坐標,并使用 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);
}
FallBack "Diffuse"
得到6張紋理的 混合權(quán)重后萨赁,對每張紋理進行采樣 并和它們對應(yīng)的權(quán)重值相乘得到每張紋理的采樣顏色。我們還計算了純白在渲染中的貢獻度兆龙,這是通過從1中減去所有6張紋理的權(quán)重來得到的杖爽。這是因為素描中往往有留白的部分,因此我們希望在最后的渲染中紫皇,光照最亮的部分是純白慰安。最后混合各個顏色值,并和陰影值 atten聪铺,模型顏色——Color相乘后返回化焕。
14.3 擴展閱讀
在工業(yè)界,非真實感渲染己被應(yīng)用到很多成功的游戲中铃剔, 除了之前提及的《大神》和《軍團要塞2》外撒桨, 還有最近的《海島奇兵》《三國志》等游戲都可以看到非其實感渲染的身影。在學術(shù)界键兜, 有更多出色的非真實感渲染的工作被提了出來元莫。讀者可以在國際討論會NPAR( Non-Photorealistic Animation and Rendering )上找到許多關(guān)于非真實感渲染的論文。浙江大學的耿衛(wèi)東教授編篡的書籍《藝術(shù)化繪制的圖形學原理與方法》(英文名: The Algorithms and Principles of Non-photorealistic Graphics)[5], 也是非常好的學習材料蝶押。這本書概述了近年來非真實感渲染在各個領(lǐng)域的發(fā)展,并簡述了許多有重要貢獻的算法過程火欧, 是一本非常好的參考書籍棋电。
在Unity 的資源商店中, 也有許多優(yōu)秀的非真實感渲染資源苇侵。例如赶盔, Toon Shader Free
( https://www.assetstore.unity3d.com/cn/#!/content/21288 )是一個免費的卡通資源包, 里面實現(xiàn)了包括輪廓線渲染等卡通風格的渲染榆浓。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 )同樣是一個需要收費的非真實感渲染效果包, 它包含了諸如鉛筆渲染烘浦、蠟筆渲染等多種手繪風格的非真實感渲染效果抖坪。