天天看點

Unity Shader 卡通渲染 (一):仿塞爾達荒野之息 Shader(簡易版)

溫馨提示:

本系列文章面向那些 Shader 剛剛入門,想尋求進一步提升的群體,如果對 Shader 一無所知的話,建議自行搜尋其他 Shader入門教程觀看學習,再食用本系列文章。

前言:

說起卡通渲染,就不得不提 《塞爾達:荒野之息》。

《塞爾達:荒野之息》可謂 2017 年的神作了,擊敗了衆多 3A 大作,成為了當年的年度遊戲。其采用的卡通渲染的美術風格也算是一大亮點(也可能是 Wii U 和 Switch 機能限制所緻)。

當年想模仿一下它的風格,可惜技術捉急…… 如今 Shader 神功已有小成,就想着嘗試一下。

因為主要在移動端開發,是以本系列文章都會采用 Vertex & Fragment Shader,非常純淨。

今後可能還會做一些其他風格的卡通渲染,不過目前就先以《塞爾達:荒野之息》的風格作為起點吧!

話不多說,先打開遊戲,截個圖作參考:

Unity Shader 卡通渲染 (一):仿塞爾達荒野之息 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 看下效果:

雖然很粗糙,但還是有那麼點感覺的。其他的視覺效果優化以後再寫吧,畢竟本篇是“簡易版”

Unity Shader 卡通渲染 (一):仿塞爾達荒野之息 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 入門精要》

繼續閱讀