1. 程式人生 > >Unity Shader入門精要學習筆記 - 第14章非真實感渲染

Unity Shader入門精要學習筆記 - 第14章非真實感渲染

只需要 遮擋 本質 lar 屏幕 準備 dot smo try

轉載自 馮樂樂的 《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中,我們會使用輪廓線顏色渲染整個背面的面片,並在視角空間下把模型頂點沿法線方向向外擴張一段距離,以此來讓背部輪廓線可見。代碼如下:

  1. viewPos = viewPos + viewNormal * _Outline;

但是,如果直接使用頂點法線進行擴展,對於一些內凹的模型,就可能法線背面面片遮擋正面面片的情況。為了盡可能防止出現這樣的情況,在擴張背面頂點之前,我們首先對頂點法線的z分量進行處理,使它們等於一個定值,然後把法線歸一化後再對頂點進行擴張。這樣的好處在於,擴展後的背面更加扁平化,從而降低了遮擋正面面片的可能性。代碼如下:

  1. viewNormal.z = -0.5;
  2. viewNormal = normalize(viewNormal);
  3. viewPos = viewPos + viewNormal * _Outline;

之前提到過,卡通風格的高光往往是模型上一塊塊分界明顯的純色區域。為了實現這種效果,我們就不能再使用之前學習的光照模型。在之前實現Blinn-Phong模型的過程中,我們使用法線點乘光照方向以及視角方向的一半,再和另一個參數進行指數操作得到高光反射系數。代碼如下:

  1. float spec = pow(max(0,dot(normal,halfDir)),_Gloss)

對於卡通渲染需要的高光反射光照模型,我們同樣需要計算normal 和 halfDir 的點乘結果,但不同的是,我們把該值和一個閾值進行比較,如果小於該閾值,則高光反射系數為0,否則返回1。

  1. float spec = dot(worldNormal, worldHalfDir);
  2. spec = step(threshold, spec)

在上面的代碼中,我們使用CG的step函數來實現和閾值比較的目的。step函數接受兩個參數,第一個參數是參考值,第二個參數是待比較的數值。如果第二個參數大於等於第一個參數,則返回1,否則返回0.

但是這種粗暴的判斷會在高光區域的邊界造成鋸齒,如下圖左圖所示。

技術分享
出現這種問題的原因在於,高光區域的邊緣不是平滑漸變的,而是由0突變到1。要想對其進行抗鋸齒處理,我們可以在邊界處很小的一塊區域內,進行平滑處理,代碼如下:

  1. float spec = dot(worldNormal, worldHalfDir);
  2. 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 代碼。

  1. Shader "Unity Shaders Book/Chapter 14/Toon Shading" {
  2. Properties {
  3. _Color ("Color Tint", Color) = (1, 1, 1, 1)
  4. _MainTex ("Main Tex", 2D) = "white" {}
  5. //控制漫反射色調的漸變紋理
  6. _Ramp ("Ramp Texture", 2D) = "white" {}
  7. //用於控制輪廓線寬度
  8. _Outline ("Outline", Range(0, 1)) = 0.1
  9. //輪廓線顏色
  10. _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
  11. //高光反射顏色
  12. _Specular ("Specular", Color) = (1, 1, 1, 1)
  13. //高光反射閾值
  14. _SpecularScale ("Specular Scale", Range(0, 0.1)) = 0.01
  15. }
  16. SubShader {
  17. Tags { "RenderType"="Opaque" "Queue"="Geometry"}
  18. //這個Pass只渲染背面的三角面片
  19. Pass {
  20. NAME "OUTLINE"
  21. //剔除正面
  22. Cull Front
  23. CGPROGRAM
  24. #pragma vertex vert
  25. #pragma fragment frag
  26. #include "UnityCG.cginc"
  27. float _Outline;
  28. fixed4 _OutlineColor;
  29. struct a2v {
  30. float4 vertex : POSITION;
  31. float3 normal : NORMAL;
  32. };
  33. struct v2f {
  34. float4 pos : SV_POSITION;
  35. };
  36. v2f vert (a2v v) {
  37. v2f o;
  38. float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
  39. float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
  40. normal.z = -0.5;
  41. pos = pos + float4(normalize(normal), 0) * _Outline;
  42. o.pos = mul(UNITY_MATRIX_P, pos);
  43. return o;
  44. }
  45. float4 frag(v2f i) : SV_Target {
  46. return float4(_OutlineColor.rgb, 1);
  47. }
  48. ENDCG
  49. }
  50. //只渲染正面
  51. Pass {
  52. Tags { "LightMode"="ForwardBase" }
  53. Cull Back
  54. CGPROGRAM
  55. #pragma vertex vert
  56. #pragma fragment frag
  57. #pragma multi_compile_fwdbase
  58. #include "UnityCG.cginc"
  59. #include "Lighting.cginc"
  60. #include "AutoLight.cginc"
  61. #include "UnityShaderVariables.cginc"
  62. fixed4 _Color;
  63. sampler2D _MainTex;
  64. float4 _MainTex_ST;
  65. sampler2D _Ramp;
  66. fixed4 _Specular;
  67. fixed _SpecularScale;
  68. struct a2v {
  69. float4 vertex : POSITION;
  70. float3 normal : NORMAL;
  71. float4 texcoord : TEXCOORD0;
  72. float4 tangent : TANGENT;
  73. };
  74. struct v2f {
  75. float4 pos : POSITION;
  76. float2 uv : TEXCOORD0;
  77. float3 worldNormal : TEXCOORD1;
  78. float3 worldPos : TEXCOORD2;
  79. SHADOW_COORDS(3)
  80. };
  81. v2f vert (a2v v) {
  82. v2f o;
  83. o.pos = mul( UNITY_MATRIX_MVP, v.vertex);
  84. o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
  85. o.worldNormal = UnityObjectToWorldNormal(v.normal);
  86. o.worldPos = mul(_Object2World, v.vertex).xyz;
  87. TRANSFER_SHADOW(o);
  88. return o;
  89. }
  90. float4 frag(v2f i) : SV_Target {
  91. fixed3 worldNormal = normalize(i.worldNormal);
  92. fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
  93. fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
  94. fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);
  95. fixed4 c = tex2D (_MainTex, i.uv);
  96. fixed3 albedo = c.rgb * _Color.rgb;
  97. fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
  98. UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
  99. fixed diff = dot(worldNormal, worldLightDir);
  100. diff = (diff * 0.5 + 0.5) * atten;
  101. fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
  102. fixed spec = dot(worldNormal, worldHalfDir);
  103. fixed w = fwidth(spec) * 2.0;
  104. fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
  105. return fixed4(ambient + diffuse + specular, 1.0);
  106. }
  107. ENDCG
  108. }
  109. }
  110. FallBack "Diffuse"
  111. }


素描風格的渲染

另一個非常流行的非真實感渲染是素描風格的渲染。微軟研究院的Praun等人在2001年發表了一篇非常著名的論文。在這篇文章中,它們使用了提前生成的素描紋理來實現實時的素描風格渲染,這些紋理組成了一個色調藝術映射(TAM)。如下圖所示,從左到右紋理匯總的筆逐漸增多,用於模擬不同光照下的漫反射效果,從上到下則對應了每張紋理的多級漸遠紋理。這些多級漸遠紋理的生成並不是簡單的對上一層紋理進行降采樣,而是需要保持筆觸之間的間隔,以便更真實地模擬素描效果。

技術分享

我們將實現一個簡化版算法,先不考慮多級漸遠紋理的生成,而直接使用6張素描紋理進行渲染。在渲染階段,我們首先在頂點著色器階段計算逐頂點的光照,根據光照結果來決定6張紋理的混合權重,並傳遞給片元著色器。然後在片元著色器中根據這些權重來混合6張紋理的采樣結果。效果如下。

技術分享

我們新建一個Shader實現上述效果。

    1. Shader "Unity Shaders Book/Chapter 14/Hatching" {
    2. Properties {
    3. _Color ("Color Tint", Color) = (1, 1, 1, 1)
    4. //紋理的平鋪系數,越大則素描線條越密
    5. _TileFactor ("Tile Factor", Float) = 1
    6. _Outline ("Outline", Range(0, 1)) = 0.1
    7. //渲染使用的6張素描紋理
    8. _Hatch0 ("Hatch 0", 2D) = "white" {}
    9. _Hatch1 ("Hatch 1", 2D) = "white" {}
    10. _Hatch2 ("Hatch 2", 2D) = "white" {}
    11. _Hatch3 ("Hatch 3", 2D) = "white" {}
    12. _Hatch4 ("Hatch 4", 2D) = "white" {}
    13. _Hatch5 ("Hatch 5", 2D) = "white" {}
    14. }
    15. SubShader {
    16. Tags { "RenderType"="Opaque" "Queue"="Geometry"}
    17. //使用之前輪廓線的Pass
    18. UsePass "Unity Shaders Book/Chapter 14/Toon Shading/OUTLINE"
    19. Pass {
    20. Tags { "LightMode"="ForwardBase" }
    21. CGPROGRAM
    22. #pragma vertex vert
    23. #pragma fragment frag
    24. #pragma multi_compile_fwdbase
    25. #include "UnityCG.cginc"
    26. #include "Lighting.cginc"
    27. #include "AutoLight.cginc"
    28. #include "UnityShaderVariables.cginc"
    29. fixed4 _Color;
    30. float _TileFactor;
    31. sampler2D _Hatch0;
    32. sampler2D _Hatch1;
    33. sampler2D _Hatch2;
    34. sampler2D _Hatch3;
    35. sampler2D _Hatch4;
    36. sampler2D _Hatch5;
    37. struct a2v {
    38. float4 vertex : POSITION;
    39. float4 tangent : TANGENT;
    40. float3 normal : NORMAL;
    41. float2 texcoord : TEXCOORD0;
    42. };
    43. struct v2f {
    44. float4 pos : SV_POSITION;
    45. float2 uv : TEXCOORD0;
    46. fixed3 hatchWeights0 : TEXCOORD1;
    47. fixed3 hatchWeights1 : TEXCOORD2;
    48. float3 worldPos : TEXCOORD3;
    49. SHADOW_COORDS(4)
    50. };
    51. v2f vert(a2v v) {
    52. v2f o;
    53. o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    54. //使用_TileFactor得到采樣坐標
    55. o.uv = v.texcoord.xy * _TileFactor;
    56. //逐頂點光照
    57. fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
    58. fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
    59. fixed diff = max(0, dot(worldLightDir, worldNormal));
    60. o.hatchWeights0 = fixed3(0, 0, 0);
    61. o.hatchWeights1 = fixed3(0, 0, 0);
    62. float hatchFactor = diff * 7.0;
    63. //計算紋理的權重
    64. if (hatchFactor > 6.0) {
    65. // Pure white, do nothing
    66. } else if (hatchFactor > 5.0) {
    67. o.hatchWeights0.x = hatchFactor - 5.0;
    68. } else if (hatchFactor > 4.0) {
    69. o.hatchWeights0.x = hatchFactor - 4.0;
    70. o.hatchWeights0.y = 1.0 - o.hatchWeights0.x;
    71. } else if (hatchFactor > 3.0) {
    72. o.hatchWeights0.y = hatchFactor - 3.0;
    73. o.hatchWeights0.z = 1.0 - o.hatchWeights0.y;
    74. } else if (hatchFactor > 2.0) {
    75. o.hatchWeights0.z = hatchFactor - 2.0;
    76. o.hatchWeights1.x = 1.0 - o.hatchWeights0.z;
    77. } else if (hatchFactor > 1.0) {
    78. o.hatchWeights1.x = hatchFactor - 1.0;
    79. o.hatchWeights1.y = 1.0 - o.hatchWeights1.x;
    80. } else {
    81. o.hatchWeights1.y = hatchFactor;
    82. o.hatchWeights1.z = 1.0 - o.hatchWeights1.y;
    83. }
    84. o.worldPos = mul(_Object2World, v.vertex).xyz;
    85. TRANSFER_SHADOW(o);
    86. return o;
    87. }
    88. fixed4 frag(v2f i) : SV_Target {
    89. //根據權重采樣取色
    90. fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x;
    91. fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y;
    92. fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z;
    93. fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x;
    94. fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y;
    95. fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z;
    96. fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z -
    97. i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
    98. fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
    99. UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    100. return fixed4(hatchColor.rgb * _Color.rgb * atten, 1.0);
    101. }
    102. ENDCG
    103. }
    104. }
    105. FallBack "Diffuse"
    106. }

Unity Shader入門精要學習筆記 - 第14章非真實感渲染