一直對非真實感渲染 (Non-Photorealistic Rendering) 很感興趣,正好發現某社出的新遊戲中可以選擇真實質感或卡通質感,所以想試試在 Unity 裏實現一下卡通着色器。
卡通渲染最關鍵的特徵包括不同於真實感渲染的藝術化光影效果和輪廓描邊。光影效果即是指將物體受光照的顏色從多色階降到低色階,減少顏色的豐富程度。本篇即討論如何卡通着色,實現該光影效果。
* 本文主要參考 Unity Assets Store 中的 Toony Colors Pro 2 ,模型也來自該工具包。着色器全部使用 Surface Shader 實現。
Github 倉庫地址: github.com/Sorumi/UnityToonShader
博文原文:sorumi.xyz/posts/unity-toon-shader/
首先搭一下基本的着色器框架,在 Surface Shader 中自定義光照模型 LightingToon,編譯指令中排除多餘的渲染路徑通道,減少最終生成 shader 的體積。
Properties
{
_Color ("Color", Color) = (1, 1, 1, 1)
_MainTex ("Main Texture", 2D) = "white" { }
}
SubShader
{
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Toon addshadow fullforwardshadows exclude_path:deferred exclude_path:prepass
#pragma target 3.0
fixed4 _Color;
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
float3 viewDir;
};
inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 normalDir = normalize(s.Normal);
float ndl = max(0, dot(normalDir, lightDir));
fixed3 lightColor = _LightColor0.rgb;
fixed4 color;
fixed3 diffuse = s.Albedo * lightColor * ndl * atten;
color.rgb = diffuse;
color.a = s.Alpha;
return color;
}
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = mainTex.rgb * _Color.rgb;
o.Alpha = mainTex.a * _Color.a;
}
ENDCG
}
這是非卡通渲染只有環境光照 (ambient) 和漫反射 (diffuse) 的效果。
簡化顏色
實現原理是把 diffuse 漫反射顏色簡化成對比較明顯的幾個色階,首先嚐試一下降到 2 階色階。Diffuse 模型中,法線方向向量與光線方向向量的點積控制着漫反射的強度。
float ndl = max(0, dot(normalDir, lightDir));
設置屬性:RampThreshold 色階閾值, 時 , 時 。但是這樣會導致分界線十分明顯,所以再增加屬性:RampSmooth 色階間平滑度。使用 smoothstep 平滑函數,根據 RampSmooth 對色階之間進行過渡。
fixed3 ramp = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, ndl);
ramp *= atten;
...
fixed3 diffuse = s.Albedo * lightColor * ramp;
可以看到光影對比很明顯了,但是這個陰影也太醜了,所以直接用顏色疊加作爲陰影和高光。設置屬性 HColor 高光顏色和 SColor 陰影顏色。
_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
...
fixed3 diffuse = s.Albedo * lightColor * rampColor;
畫面一下子就變得乾淨了,看起來舒服多啦~
增加鏡面高光和邊緣光
之後再做一些增強畫面效果的工作,首先設置鏡面光照的相關屬性:SpecColor 高光顏色、SpecSmooth 高光色階的平滑度、Shininess 鏡面反射度。
surf 着色器裏,使用紋理的 alpha 通道作爲光澤度 Gloss 。
void surf(Input IN, inout SurfaceOutput o)
{
...
o.Specular = _Shininess;
o.Gloss = mainTex.a;
}
inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
half3 halfDir = normalize(lightDir + viewDir);
...
float ndh = max(0, dot(normalDir, halfDir));
...
float spec = pow(ndh, s.Specular * 128.0) * s.Gloss;
spec *= atten;
spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
...
fixed3 specular = _SpecColor.rgb * lightColor * spec;
color.rgb = diffuse + specular;
color.a = s.Alpha;
return color;
}
設置邊緣光的屬性:RimColor 邊緣光顏色、RimThreshold 邊緣光閾值、RimSmooth 邊緣光色階的平滑度。法線方向與視線方向夾角越小,與光線方向夾角越大,則邊緣光強度越強。
inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
...
float ndv = max(0, dot(normalDir, viewDir));
...
float rim = (1.0 - ndv) * ndl;
rim *= atten;
rim = smoothstep(_RimThreshold - _RimSmooth * 0.5, _RimThreshold + _RimSmooth * 0.5, rim);
...
fixed3 rimColor = _RimColor.rgb * lightColor * _RimColor.a * rim;
color.rgb = diffuse + specular + rimColor;
color.a = s.Alpha;
return color;
}
陰影紋理
對於模型不同部分,單純用顏色來做陰影可能缺少層次感,可以考慮使用紋理,對不同部分添加不同的陰影顏色。這裏需要自定義 SurfaceOutput ,添加 Shadow 保存紋理採樣的顏色。
void surf(Input IN, inout SurfaceOutputCustom o)
{
...
fixed4 shadowTex = tex2D(_ShadowTex, IN.uv_MainTex);
o.Shadow = shadowTex.rgb;
...
}
inline fixed4 LightingToon(SurfaceOutputCustom s, half3 lightDir, half3 viewDir, half atten)
{
...
s.Albedo = lerp(s.Shadow, s.Albedo, ramp);
...
}
多階色階
一般,多階色階是由美術製作一維色彩表的紋理,對其進行採樣獲得顏色值。現在來考慮一下用程序來實現。
同樣使用 RampThreshold 控制光影的比例。
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
增加屬性 ToonSteps 表示色階層數。
float ramp = floor(diff * _ToonSteps) / _ToonSteps;
色階之間需要根據 RampSmooth 平滑過渡,首先來看一下一直使用的 smoothstep 函數。
smoothstep(float min, float max, float t);
根據 t , min 和 max ,返回在 0 到 1之間的一個數,類似於插值函數 lerp 。但是 lerp 是線性,而 smoothstep 在min和 max 處增長緩慢,中間增長較快。
如果 ToonSteps = 5 ,則 ramp 關於 diff 的函數圖像爲
float interval = 1 / _ToonSteps;
float level = round(diff * _ToonSteps) / _ToonSteps;
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *= atten;
但是當 RampSmooth = 1 ,即完全平滑,圖像應該是線性的,但是函數圖像如下圖
爲了修正這一點,暫時的做法是加了一個判斷
float linearstep(float min, float max, float t)
{
return saturate((t - min) / (max - min));
}
inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
...
if (_RampSmooth == 1)
{
ramp = interval * linearstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
}
else
{
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
}
...
}
效果:
參考鏈接
Toony Colors Pro 2
卡通渲染及其相關技術總結
Unity3d shader之卡通着色Toon Shading