非真實感渲染(Non Photorealistic Rendering,NPR),用於卡通,水彩風格渲染。
卡通風格渲染(黑色描邊,分明的明暗變化)
基於色調的着色技術(tone-based shading),使用藍色來模擬冷色調,使用黃色來模擬黃色調,來實現從冷到暖的色調變化,爲了更加真實的模擬,還可以模型本身的漫反射顏色進行混合來得到最終的冷暖色調。
實現中一般使用漫反射係數對一張一維紋理進行採樣,以控制漫反射色調。
1.渲染輪廓線(5種方式)
- 基於觀察角度和表面法線的輪廓線渲染。這種方法使用視角方向和表面法線的點乘結果來得到輪廓線的信息。快速簡單,使用一個Pass就可得到結果。但是描邊效果不好。
- 過程式幾何輪廓線渲染。使用兩個Pass渲染,第一個Pass渲染背面,並使用它的輪廓可見。第二個Pass渲染正面,這種方法快速,效果好,但是不適合類似立方體這樣的平整模型。
- 基於圖像處理的輪廓線渲染。屏幕後處理。適用於任何模型,但是深度和法線變化比較小的輪廓無法檢測出來。比如桌子上的紙張。
- 基於輪廓邊檢測的輪廓線渲染。本質是判斷相鄰兩個三角面片是否朝一個朝正面,一個朝背面。由於是逐幀提取輪廓線,幀與幀之間會出現跳躍性。
- 基於上述幾種渲染方法,先找到輪廓邊,把模型和輪廓渲染到紋理中。再使用圖像處理方法識別出輪廓線,並在圖像空間進行風格化渲染。
基於過程式幾何輪廓線渲染:
第一個Pass中,使用輪廓線顏色渲染整個背面的面片,並在視角空間下把模型頂點沿着法線方向向外擴張一段距離,以此讓背部輪廓線可見。代碼如下:
viewPos = viewPos + viewNormal * _Outline
對於內凹模型,直接用頂點法線進行擴展,可能會背面面片遮擋正面面片,爲了防止出現這種情況,擴張背面頂點,先把頂點法線等於一個定值,然後歸一化法線,再對頂點進行擴張。是擴張後的背面更加扁平化,降低遮擋正面面片的可能性。
viewNormal.z = -0.5
viewNormal = normalize(viewNormal)
viewPos = viewPos + viewNormal * _Outline;
儘量讓頂點沿xy平面擴張,不擴張z,然後背面面片z值大於前面面片,導致背面面片,遮住前面面片。
2.添加高光
爲了讓模型有分界明顯的純色區域,使用法線點乘(光照方向+視角方向)的一半,再和一個閾值進行比較,小於該閾值,高光反射係數爲0,大於則爲1。
float spec = dot(worldNormal,worldHalfDir);
spec = step(閾值,spec);// step函數 spec小於閾值返回0,大於等於返回1
這種方式會讓高光區域有鋸齒,因爲高光區域邊緣從0突變到1,沒有漸變。因此高光邊緣區域加個漸變,進行抗鋸齒處理。
float spec = dot(worldNormal,worldHalfDir);
spec = lerp(0,1,smothstep(-w,w,spec - 閾值)) //spec - 閾值 < -w,返回0,大於w返回1,否則在0到1之間進行插值。
這樣[-w,w]區間內,就有個漸變插值,消除鋸齒。
Shader "Chan/Chapter14_ToonShading" {
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) = (1,1,1,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名稱,其他Shader可複用
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);
//法線從模型空間變換到攝像機空間 爲什麼不能用UNITY_MATRIX_MV?
//https://blog.csdn.net/a133900029/article/details/80558765
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
//防止背後面片頂點擴張,z值比前面面片頂點的z值大,出現遮擋情況
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 = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
//世界空間下的法線和頂點座標
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,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);
//材質反射率albedo
fixed3 albedo = c.rgb * _Color.rgb;
//環境光
fixed ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//對陰影紋理進行採樣
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//漫反射光照計算
fixed diff = dot(worldNormal,worldLightDir);
//半蘭伯特漫反射光照模型計算漫反射係數diff,使diff在【0,1】區間內。
//漸變紋理縱軸顏色不變,直接用[diff,diff]對漸變紋理採樣
diff = (diff * 0.5 + 0.5) * atten;
//半蘭伯特漫反射 * 陰影 = 最終的漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp,float2(diff,diff)).rgb;
//高光反射
fixed3 spec = dot(worldNormal,worldHalfDir);
//鄰域像素之間的近似導數值
fixed w = fwidth(spec) * 2.0;
//高光顏色 * 高光反射係數 = 高光反射顏色
//smoothstep在[-w,w]之間進行插值
//step(0.0001,_SpecularScale)) _SpecularScale < 0.0001,返回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"
}
註解:
第一個Pass,在攝像機空間下,將頂點從處理過的法線方向,外擴。
第二個Pass,計算 環境光+漫反射+高光反射 = 最終顏色。
素描風格的渲染
Shader "Chan/Chapter14_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"}
//使用Chan/Chapter14_ToonShading Shader中的Pass渲染模型輪廓
UsePass "Chan/Chapter14_ToonShading/OUTLINE"
Pass
{
//爲了獲得各個光照變量,需要設置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;
//6張紋理 需要6個混合權重,存儲在以下兩個變量中
fixed3 hatchWeights0:TEXCOORD1;
fixed3 hatchWeights1:TEXCOORD2;
//爲了添加陰影效果,還聲明瞭worldPos變量
float3 worldPos:TEXCOORD3;
//申明陰影紋理的採樣座標 (其實就是TEXCOORD4)
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//
o.uv = v.texcoord.xy * _TileFactor;
//世界空間下,dot(光照方向,法線方向) = 漫反射係數
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed diff = max(0,dot(worldLightDir,worldNormal));
//6張紋理權重初始化爲0
o.hatchWeights0 = fixed3(0,0,0);
o.hatchWeights1 = fixed3(0,0,0);
float hatchFactor = diff * 7.0;
//根據hatchFactor決定紋理混合權重
if(hatchFactor > 6.0)
{
//不處理,光照最亮部分 後邊做純白處理
//六個紋理的權重,存儲在hatchWeights0 hatchWeights1中的xyz分量中
}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(unity_ObjectToWorld,v.vertex).xyz;
//計算陰影紋理的採樣座標(TEXCOORD4)
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);
//經過權重採樣後的6張紋理採樣顏色 和 純白顏色
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
//計算陰影和光照衰減
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
return fixed4(hatchColor.xyz * _Color.rgb * atten,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
註解:
//uv範圍放大,然後跟每個紋理對應的權重對比,在權重範圍內,採樣對應的紋理
o.uv = v.texcoord.xy * _TileFactor;