天天看点

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 入门精要》

继续阅读