Unity Shader系列文章:Unity Shader目錄-初級篇
Unity Shader系列文章:Unity Shader目錄-中級篇
效果:
游戲《大神》(英文名:Okami)的游戲截圖
卡通風格的渲染效果
左圖:未對高光區(qū)域進行抗鋸齒處理扰法。右圖:使用fwidth函數(shù)對高光區(qū)域進行抗鋸齒處理
原理:
1煤篙、渲染輪廓線剩岳;
2芍躏、添加高光;
繪制模型輪廓線的方法有5種類型:
- 基于觀察角度和表面法線的輪廓線渲染糊饱。這種方法使用視角方向和表面法線的點乘結果來得到輪廓線的信息。這種方法簡單快速,可以在 Pass 中就得到渲染結果祟偷,但局限性很大,很多模型渲染出來的描邊效果都不盡如人意打厘。
- 過程式幾何輪廓線渲染修肠。這種方法的核心是使用兩個 Pass 渲染。第一個Pass渲染背面的面片户盯,并使用某些技術讓它的輪廓可見嵌施;第二個 Pass 再正常渲染正面的面片。這種方法的優(yōu)點在于快速有效莽鸭,并且適用于絕大多數(shù)表面平滑的模型吗伤,但它的缺點是不適合類似于立方體這樣平整的模型。
- 基于圖像處理的輪廓線渲染硫眨。使用卷積核進行邊界檢測足淆。這種方法的優(yōu)點在于可以適用于任何種類的模型。但它也有自身的局限所在礁阁,一些深度和法線變化很小的輪廓無法被檢測出來巧号,例如桌子上的紙張。
- 基于輪廓邊檢測的輪廓線渲染姥闭。上面提到的各種方法丹鸿,一個最大的問題是,無法控制輪廓
線的風格渲染棚品。對于一些情況靠欢,我們希望可以渲染出獨特風格的輪廓線,例如水墨風格等铜跑。為此门怪,我們希望可以檢測出精確的輪廓邊,然后直接渲染它們锅纺。檢測一條邊是否是輪廓邊的公式很簡單掷空,我們只需要檢查和這條邊相鄰的兩個三角面片是否滿足以下條件:
其中,和
分別表示兩個相鄰三角面片的法向伞广, 是從視角到該邊上任意頂點的方向拣帽。上述公式的本質在于檢查兩個相鄰的三角面片是否一個朝正面疼电、一個朝背面嚼锄。我們可以在幾何著色器(Geometry hader 的幫助下實現(xiàn)上面的檢測過程。當然蔽豺,這種方法也有缺點区丑,除了實現(xiàn)相對復雜外,它還會有動畫連貫性的問題。也就是說沧侥,由于是逐幀單獨提取輪廓可霎,所以在幀與幀之間會出現(xiàn)跳躍性。
- 最后一個種類就是混合了上述的幾種渲染方法宴杀。例如癣朗,首先找到精確的輪廓邊,把模型和輪廓邊渲染到紋理中旺罢,再使用圖像處理的方法識別出輪廓線旷余,并在圖像空間下進行風格化渲染。
在本篇中扁达,將會在 Unity 中使用過程式幾何輪廓線渲染的方法來對模型進行輪廓描邊正卧。將使用兩個 Pass 渲染模型:在第一個Pass中,我們會使用輪廓線顏色渲染整個背面的面片跪解,在視角空間下把模型頂點沿著法線方向向外擴張一段距離炉旷,以此來讓背部輪廓線可見。
shader代碼:
// 卡通渲染效果
Shader "Custom/ToonShading"
{
Properties
{
_Color ("ColorTint", Color) = (1, 1, 1, 1)
_MainTex ("Texture", 2D) = "white" { }
_Ramp ("Ramp Texture", 2D) = "white" { }// 控制漫反射色調的漸變紋理
_Outline ("Outline", Range(0, 1)) = 0.1 // 控制輪廓線寬度
_OutlineColor ("OutlineColor", Color) = (0, 0, 0, 1) // 輪廓線顏色
_Specular ("Specular", Color) = (1, 1, 1, 1) // 高光反射顏色
_SpecularScale ("SpecularScale", Range(0, 0.1)) = 0.01 // 控制計算高光反射區(qū)域大小
}
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
// 定義渲染輪廓線需要的 Pass
Pass
{
// 使用 NAME 命令為該 Pas 定義了名稱叉讥,這樣別的Shader可以通過此名字調用此Pass
// 因為描邊在非真實感渲染中是非常常見的效果 為該Pass義名稱可以讓我們在后面的使用中不需要再重復編寫此 Pass
// 而只需要調用它的名字即可窘行,如: UsePass "Cusstom/ToonShading/OUTLINE"
NAME "OUTLINE"
// 剔除正面,只渲染背面
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
fixed4 _OutlineColor;
// 應用傳遞給定點著色器的數(shù)據(jù)
struct a2v
{
float4 vertex: POSITION; // 語義:模型空間下的頂點坐標
float4 normal: NORMAL; // 語義:模型空間下的法線
};
// 頂點著色器傳遞給片元著色器的數(shù)據(jù)
struct v2f
{
float4 pos: SV_POSITION; // 語義:裁剪空間下的頂點坐標
};
// 頂點著色器函數(shù)
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;
}
// 片元著色器函數(shù)
fixed4 frag(v2f i): SV_TARGET
{
// 用輪廓線顏色渲染整個背面
return fixed4(_OutlineColor.rgb, 1);
}
ENDCG
}
// Base Pass 計算平行光图仓、環(huán)境光
Pass
{
Tags { "LightMode" = "ForwardBase" }
Cull Back
CGPROGRAM
// 編譯指令抽高,保證在pass中得到Pass中得到正確的光照變量
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#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;
// 應用傳遞給定點著色器的數(shù)據(jù)
struct a2v
{
float4 vertex: POSITION; // 語義: 頂點坐標
float3 normal: NORMAL; // 語義: 法線
float4 texcoord: TEXCOORD0; // 語義: 紋理坐標
float4 tangent: TANGENT; // 語義: 切線
};
// 頂點著色器傳遞給片元著色器的數(shù)據(jù)
struct v2f
{
float4 pos: SV_POSITION; // 語義: 裁剪空間的頂點坐標
float2 uv: TEXCOORD0;
float3 worldNormal: TEXCOORD1;
float3 worldPos: TEXCOORD2;
SHADOW_COORDS(3) // 內置宏:聲明一個用于對陰影紋理采樣的坐標 (這個宏參數(shù)需要是下一個可用的插值寄存器的索引值,這里是3)
};
// 頂點著色器
v2f vert(a2v v)
{
v2f o;
// 將頂點坐標從模型空間變換到裁剪空間
// 等價于o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.pos = UnityObjectToClipPos(v.vertex);
// 計算紋理坐標(縮放和平移)
// 等價于o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
// 將法線從模型空間變換到世界空間
// 等價于o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
// 將頂點坐標從模型空間變換到世界空間
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 內置宏:用于計算聲明的陰影紋理坐標
TRANSFER_SHADOW(o);
return o;
}
// 片元著色器
fixed4 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;
// 環(huán)境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
// 計算陰影值和光照衰減
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// 計算半蘭伯特漫反射系數(shù)
fixed diff = dot(worldNormal, worldLightDir);
// 和陰影值相乘得到最終的漫反射系數(shù)
diff = (diff * 0.5 + 0.5) * atten;
// 使用漫反射系數(shù)對漸變紋理_Ramp進行采樣翘骂,并將結果和材質的反射率、光照顏色相乘 作為最后的漫反射光照
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
fixed spec = dot(worldNormal, worldHalfDir);
// 高光區(qū)域的邊緣不是平滑漸變的帚豪,而是由0突變到1碳竟,對其進行抗鋸齒處理,可以在邊界處很小的一塊區(qū)域內進行平滑處理
// 使用smoothstep函數(shù)狸臣,w個很小的值莹桅,當spec-threshold小于-w 時,返回0烛亦;大于w時诈泼,返回1;否則在0-1之間進行插值煤禽。
// 這樣的效果是铐达,我們可以在[-w, w] 區(qū)間內,即高光區(qū)域的邊界處檬果,得到一個從0到1平滑變化的 spec 值瓮孙,從而實現(xiàn)抗鋸齒的目的
// 盡管我可以把w設為一個很小的定值唐断,不過更好的是選擇使用鄰域像素之間的近似導數(shù)值,這可以通過 fwidth 函數(shù)來得到
// 使用step(0.0001, _SpecularScale)杭抠,這是為了在 SpecularScaJe 可以完全消除高光反射的光照
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);
}
ENDCG
}
}
// 設置Fallback脸甘,產生正確的陰影投射效果
Fallback "Diffuse"
}