Unity Shader入門精要學習筆記 - 第14章非真實感渲染
轉載自 馮樂樂的 《Unity Shader 入門精要》
盡管遊戲渲染一般都是以照相寫實主義作為主要目標,但也有許多遊戲使用了非真實感渲染(NPR)的方法來渲染遊戲畫面。非真實感渲染的一個主要目標是,使用一些渲染方法使得畫面達到和某些特殊的繪畫風格相似的效果,例如卡通、水彩風格等。
卡通風格的渲染
卡通風格是遊戲中常見的一種渲染風格。使用這種風格的遊戲畫面通常有一些共有的特點,例如物體都被黑色的線條描邊,以及分明的明暗變化等。如下圖所示。
要實現卡通渲染有很多方法,其中之一就是使用基於色調的著色技術。在實現中,我們往往會使用漫反射系數對一張一維紋理進行采樣,以控制漫反射的色調。卡通風格的高光效果也和我們之前學習的光照不同。在卡通風格總,模型的高光往往是一塊塊分界明顯的純色風格。
除了光照模型不同外,卡通風格通常還需要在物體邊緣部分繪制輪廓。之前我們已經介紹過使用屏幕後處理結束對屏幕圖像進行描邊。在這裏,我們將介紹基於模型的描邊方法,這種方法的實現更加簡單,而且很多情況下也能得到不錯的效果。效果如下圖。
在實時渲染中,輪廓線的渲染是應用非常廣泛的一種效果。在《Real Time Rendering, third edition》中,作者把繪制模型輪廓線的方法分為5種。
1)基於觀察角度和表面法線的輪廓線渲染。(這種方法使用視角方向和表面法線的點乘結果來得到輪廓線的信息。這種方法簡單快速,可以在一個Pass中就得到渲染結果,但局限性很大,很多模型渲染出來的描邊效果都不盡如人意)
2)過程式幾何輪廓線渲染。這種方法的核心是使用兩個Pass渲染。第一個Pass渲染背面的面片,並使用某些技術讓它的輪廓可見;第二個Pass再正常渲染正面的面片。這種方法的有點在於快速有效,並且適用於絕大多數表面平滑的模型,但它的缺點是不適合類似於立方體這樣平整的模型。
3)基於圖像處理的輪廓線渲染。有點在於,可以適用於任何種類的模型。局限在於,一些深度和法線變化很小的輪廓無法被檢測出來,例如桌子上的紙張。
4)基於輪廓邊檢測的輪廓線渲染。上面提到的方法,一個最大的問題是,無法控制輪廓線的風格渲染。對於一些情況,我們希望可以渲染出獨特風格的輪廓線,例如水墨風格等。為此,我們希望可以檢測出精確的輪廓邊,然後直接渲染它們。檢測一條邊是否是輪廓邊的公式很簡單,我們只需要檢查和這條邊相鄰的兩個三角面片是否滿足以下條件
其中,n(0)和n(1)分別表示兩個相鄰三角面片的法向,v 是從視角到該邊上任意頂點的方向。上述公式的本質在於檢查兩個相鄰三角面片是否一個朝正面,一個朝背面。我們可以在幾何著色器的幫助下實現上面的檢測過程。當然,這種方法也有缺點,除了實現相對復雜外,它還會有動畫連貫性的問題。也就是說,由於是逐幀單獨提取輪廓,所以在幀與幀之間會出現跳躍性。
5)最後一種就是混合了上述的幾種渲染方法。例如,首先找到精確的輪廓邊,把模型和輪廓邊渲染到紋理中,再使用圖像處理的方法識別輪廓線,並在圖像空間下進行風格化渲染。
我們使用過程幾何輪廓線渲染的方法來對模型進行輪廓描邊。我們將使用兩個Pass渲染模型:在第一個Pass中,我們會使用輪廓線顏色渲染整個背面的面片,並在視角空間下把模型頂點沿法線方向向外擴張一段距離,以此來讓背部輪廓線可見。代碼如下:
- viewPos = viewPos + viewNormal * _Outline;
但是,如果直接使用頂點法線進行擴展,對於一些內凹的模型,就可能法線背面面片遮擋正面面片的情況。為了盡可能防止出現這樣的情況,在擴張背面頂點之前,我們首先對頂點法線的z分量進行處理,使它們等於一個定值,然後把法線歸一化後再對頂點進行擴張。這樣的好處在於,擴展後的背面更加扁平化,從而降低了遮擋正面面片的可能性。代碼如下:
- viewNormal.z = -0.5;
- viewNormal = normalize(viewNormal);
- viewPos = viewPos + viewNormal * _Outline;
之前提到過,卡通風格的高光往往是模型上一塊塊分界明顯的純色區域。為了實現這種效果,我們就不能再使用之前學習的光照模型。在之前實現Blinn-Phong模型的過程中,我們使用法線點乘光照方向以及視角方向的一半,再和另一個參數進行指數操作得到高光反射系數。代碼如下:
- float spec = pow(max(0,dot(normal,halfDir)),_Gloss)
對於卡通渲染需要的高光反射光照模型,我們同樣需要計算normal 和 halfDir 的點乘結果,但不同的是,我們把該值和一個閾值進行比較,如果小於該閾值,則高光反射系數為0,否則返回1。
- float spec = dot(worldNormal, worldHalfDir);
- spec = step(threshold, spec)
在上面的代碼中,我們使用CG的step函數來實現和閾值比較的目的。step函數接受兩個參數,第一個參數是參考值,第二個參數是待比較的數值。如果第二個參數大於等於第一個參數,則返回1,否則返回0.
但是這種粗暴的判斷會在高光區域的邊界造成鋸齒,如下圖左圖所示。
出現這種問題的原因在於,高光區域的邊緣不是平滑漸變的,而是由0突變到1。要想對其進行抗鋸齒處理,我們可以在邊界處很小的一塊區域內,進行平滑處理,代碼如下:
- float spec = dot(worldNormal, worldHalfDir);
- spec = lerp(0,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 函數來得到。
當然,卡通渲染的高光往往有更多個性化的需要。例如很多卡通高光特效希望可以隨意伸縮、方塊話光照區域等。
為了實現上述效果,我們做如下準備工作。
1)新建一個場景,去掉天空盒子
2)新建一個材質,新建一個Shader ,賦給材質
3)場景中新建一個Suzanne 模型,材質賦給模型。
我們修改Shader 代碼。
- Shader "Unity Shaders Book/Chapter 14/Toon Shading" {
- 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
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- //這個Pass只渲染背面的三角面片
- Pass {
- NAME "OUTLINE"
- //剔除正面
- Cull Front
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #include "UnityCG.cginc"
- float _Outline;
- fixed4 _OutlineColor;
- struct a2v {
- 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);
- }
- ENDCG
- }
- //只渲染正面
- Pass {
- Tags { "LightMode"="ForwardBase" }
- Cull Back
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma multi_compile_fwdbase
- #include "UnityCG.cginc"
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- #include "UnityShaderVariables.cginc"
- fixed4 _Color;
- sampler2D _MainTex;
- float4 _MainTex_ST;
- sampler2D _Ramp;
- fixed4 _Specular;
- fixed _SpecularScale;
- struct a2v {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 texcoord : TEXCOORD0;
- float4 tangent : TANGENT;
- };
- 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;
- }
- 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);
- }
- ENDCG
- }
- }
- FallBack "Diffuse"
- }
素描風格的渲染
另一個非常流行的非真實感渲染是素描風格的渲染。微軟研究院的Praun等人在2001年發表了一篇非常著名的論文。在這篇文章中,它們使用了提前生成的素描紋理來實現實時的素描風格渲染,這些紋理組成了一個色調藝術映射(TAM)。如下圖所示,從左到右紋理匯總的筆逐漸增多,用於模擬不同光照下的漫反射效果,從上到下則對應了每張紋理的多級漸遠紋理。這些多級漸遠紋理的生成並不是簡單的對上一層紋理進行降采樣,而是需要保持筆觸之間的間隔,以便更真實地模擬素描效果。
我們將實現一個簡化版算法,先不考慮多級漸遠紋理的生成,而直接使用6張素描紋理進行渲染。在渲染階段,我們首先在頂點著色器階段計算逐頂點的光照,根據光照結果來決定6張紋理的混合權重,並傳遞給片元著色器。然後在片元著色器中根據這些權重來混合6張紋理的采樣結果。效果如下。
我們新建一個Shader實現上述效果。
- Shader "Unity Shaders Book/Chapter 14/Hatching" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- //紋理的平鋪系數,越大則素描線條越密
- _TileFactor ("Tile Factor", Float) = 1
- _Outline ("Outline", Range(0, 1)) = 0.1
- //渲染使用的6張素描紋理
- _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" {}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- //使用之前輪廓線的Pass
- UsePass "Unity Shaders Book/Chapter 14/Toon Shading/OUTLINE"
- Pass {
- Tags { "LightMode"="ForwardBase" }
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- #pragma multi_compile_fwdbase
- #include "UnityCG.cginc"
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- #include "UnityShaderVariables.cginc"
- fixed4 _Color;
- float _TileFactor;
- sampler2D _Hatch0;
- sampler2D _Hatch1;
- sampler2D _Hatch2;
- sampler2D _Hatch3;
- sampler2D _Hatch4;
- sampler2D _Hatch5;
- struct a2v {
- float4 vertex : POSITION;
- float4 tangent : TANGENT;
- float3 normal : NORMAL;
- float2 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- fixed3 hatchWeights0 : TEXCOORD1;
- fixed3 hatchWeights1 : TEXCOORD2;
- float3 worldPos : TEXCOORD3;
- SHADOW_COORDS(4)
- };
- v2f vert(a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //使用_TileFactor得到采樣坐標
- 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;
- }
- 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);
- }
- ENDCG
- }
- }
- FallBack "Diffuse"
- }
Unity Shader入門精要學習筆記 - 第14章非真實感渲染