溫馨提示:
本系列文章面向那些 Shader 剛剛入門,想尋求進一步提升的群體,如果對 Shader 一無所知的話,建議自行搜尋其他 Shader入門教程觀看學習,再食用本系列文章。
前言:
說起卡通渲染,就不得不提 《塞爾達:荒野之息》。
《塞爾達:荒野之息》可謂 2017 年的神作了,擊敗了衆多 3A 大作,成為了當年的年度遊戲。其采用的卡通渲染的美術風格也算是一大亮點(也可能是 Wii U 和 Switch 機能限制所緻)。
當年想模仿一下它的風格,可惜技術捉急…… 如今 Shader 神功已有小成,就想着嘗試一下。
因為主要在移動端開發,是以本系列文章都會采用 Vertex & Fragment Shader,非常純淨。
今後可能還會做一些其他風格的卡通渲染,不過目前就先以《塞爾達:荒野之息》的風格作為起點吧!
話不多說,先打開遊戲,截個圖作參考:
由圖可見,塞爾達荒野之息的卡通渲染十分簡單明快,主要有三個要點:亮部、暗部、邊緣光
當然仔細看的話,頭發是有高光且有特殊處理的,不過這篇如題 “簡易版”,就不考慮那麼多了,先把上邊這三點做完。
一、準備工作
首先,在目錄下建立一個 Unlit Shader。
擷取三個常用素材,法線 N,光照方向 L,視角方向 V。
熟悉 Shader 的一定知道,這裡就不多說了,直接貼代碼:
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
UNITY_FOG_COORDS(3)
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
...
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
...
}
fixed4 frag (v2f i) : SV_Target
{
...
fixed3 worldNormal = normalize(i.worldNormal); //法線 N
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); //光照方向 L
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); //視角方向 V
...
}
二、亮部、暗部
已知法線 N,光照方向 L。可用 N · L 來區分亮部暗部
若值小于0,則背光,判斷為暗部,若值大于0,則迎光,判斷為亮部。
當然也可以自己定義門檻值,不一定要為0。
fixed diffValue = dot(worldNormal, worldLightDir);
fixed diffStep = step(_ShadowThreshold, diffValue);
三、邊緣光
已知法線 N,視角方向 V。可用 N · V 的值來區分是否是物體邊緣,
值越接近零,N 與 V 越接近垂直,則是邊緣。這裡多加了一個 Pow 計算,防止出現整片的邊緣光,個人感覺效果較好。
(這邊沒使用參考文章裡的做法,感覺效果不好)
另外記得背光面不用邊緣光;乘以 0.5 是希望不要過曝,可以帶點原本貼圖的顔色。
fixed rimValue = pow(1 - dot(worldNormal, worldViewDir), _RimPower);
fixed rimStep = step(_RimThreshold, rimValue);
fixed4 rim = rimStep * 0.5 * diffStep * _RimColor;
四、其他
一些簡單的光照參數還是要的,這樣直接調 Direction Light 顔色就能影響所有模型,做場景氣氛會很有用。
光照這邊模仿一下半蘭伯特(會明亮一點,光照強度 0 時不會全黑)。
不過這邊不打算加環境光,因為很容易過曝,不好調色,想加的同學自己加就好。
fixed4 light = _LightColor0 * 0.5 + 0.5;
fixed4 diffuse = light * col * (diffStep + (1 - diffStep) * _ShadowBrightness) * _Color;
...
fixed4 rim = light * rimStep * 0.5 * diffStep * _RimColor;
五、成果
在 Asset Store 裡下了個免費的小姐姐模型(搜尋 Anime Girl Idle Animations Free),換上 Shader 看下效果:
雖然很粗糙,但還是有那麼點感覺的。其他的視覺效果優化以後再寫吧,畢竟本篇是“簡易版”
完整 Shader 代碼如下,謝謝觀賞!~(歡迎各位觀衆給出寶貴意見)
Shader "Custom/ToonShadingSimple"
{
Properties
{
[Header(Main)]
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_RimColor ("RimColor", Color) = (1.0, 1.0, 1.0, 1.0)
_ShadowThreshold ("ShadowThreshold", Range(-1.0, 1.0)) = 0.2
_ShadowBrightness ("ShadowBrightness", Range(0.0, 1.0)) = 0.6
_RimThreshold ("RimThreshold", Range(0.0, 1.0)) = 0.35
_RimPower ("RimPower", Range(0.0, 16)) = 4.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Cull Back
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
UNITY_FOG_COORDS(3)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
fixed4 _RimColor;
fixed _ShadowThreshold;
fixed _ShadowBrightness;
fixed _RimThreshold;
half _RimPower;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal); //法線 N
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); //光照方向 L
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); //視角方向 V
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
fixed diffValue = dot(worldNormal, worldLightDir);
fixed diffStep = step(_ShadowThreshold, diffValue);
fixed4 light = _LightColor0 * 0.5 + 0.5;
fixed4 diffuse = light * col * (diffStep + (1 - diffStep) * _ShadowBrightness) * _Color;
// 模仿參考文章的方法,感覺效果不是太好
// fixed rimValue = 1 - dot(worldNormal, worldViewDir);
// fixed rimStep = step(_RimThreshold, rimValue * pow(dot(worldNormal,worldLightDir), _RimPower));
fixed rimValue = pow(1 - dot(worldNormal, worldViewDir), _RimPower);
fixed rimStep = step(_RimThreshold, rimValue);
fixed4 rim = light * rimStep * 0.5 * diffStep * _RimColor;
fixed4 final = diffuse + rim;
// apply fog
UNITY_APPLY_FOG(i.fogCoord, final);
return final;
}
ENDCG
}
}
}
下一篇傳送門:
https://blog.csdn.net/qq_27534999/article/details/100925621
參考資料:
1、https://roystan.net/articles/toon-shader.html
2、《Unity Shader 入門精要》