温馨提示:
本系列文章面向那些 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 入门精要》