从荒野之息林克的卡通着色中,主要有三个部分:
- 多色阶着色(荒野之息是双色阶)
- 高光反射
- 边缘光
荒野之息的 Toon Shader 分析,来自Roystan的教程
Cel Shading
先从基础的漫反射开始:
shader "Custom/ToonShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"LightMode" = "ForwardBase"
}
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
float3 lightDir : TEXCOORD2;
float3 viewDir : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normal = v.normal;
o.lightDir = ObjSpaceLightDir(v.vertex);
o.viewDir = ObjSpaceViewDir(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 normalDir = normalize(i.normal);
fixed3 lightDir = normalize(i.lightDir);
fixed3 viewDir = normalize(i.viewDir);
// 环境光
fixed4 ambient = UNITY_LIGHTMODEL_AMBIENT;
// 采样物体颜色
fixed4 objColor = _Color * tex2D(_MainTex, i.uv);
// 兰伯特定律
float NdotL = dot(normalDir, lightDir);
// 漫反射
fixed4 diffuse = max(NdotL, 0);
// 物体着色 + 环境光 + 高光 + 边缘光
return diffuse * _LightColor0 * objColor + ambient * objColor;
}
ENDCG
}
}
}
标准的兰伯特漫反射模型,显示出来的物体光照只有漫反射和环境光的效果。
加入色阶控制:
// two steps
fixed4 rampColor = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, diffuse);
// 物体着色 + 环境光
return rampColor * lightCol * _LightColor0 + ambient * objColor;
物体着色分成两个色阶。不过阴影处的颜色太深了,需要控制一下。 设置好最亮的颜色和阴影处的颜色,然后把着色映射到区间:
// two steps
fixed4 rampColor = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, diffuse);
// 控制阴影的颜色
rampColor = lerp(_ShadowColor, _BrightColor, rampColor);
PS:之前一直以为 smoothstep 是线性映射。。。 y = smoothstep(0, 1, x) 和 y = x 对比,来自Roystan的教程
高光
加上高光的相关属性,高光颜色 SpecColor ,光泽度 shinness 和 高光边缘平滑度 SpecSmooth。使用blinn光照模型,通过NdotH求出高光范围:
// blinn 光照模型
float3 halfVector = normalize(lightDir + viewDir);
float NdotH = dot(normalDir, halfVector);
// 高光亮度
float specPower = pow(NdotH, _Shinness * 128);
// 高光边缘模糊调整
float4 specular = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, specPower);
// 物体着色 + 环境光 + 高光
return rampColor * _LightColor0 * objColor + ambient * objColor + specular * _SpecColor * _LightColor0 * objColor;
PS:高光的形状也是可以定制的,在非真实渲染中也是常用的技巧,还有头发上的各向异性的高光。(再说吧)
边缘光
同样加入边缘光的相关属性,边缘光颜色 RimColor,边缘光平滑度 RimSmooth,边缘光阈值 RimThreshold:
// 边缘光:法线和视线的夹角越小,与光线夹角越大,边缘光越强
float rimPower = (1 - dot(viewDir, normalDir)) * NdotL;
// 边缘模糊
rimPower = smoothstep(_RimThreshold - _RimSmooth * 0.5, _RimThreshold + _RimSmooth * 0.5, rimPower);
float4 rim = rimPower * _RimColor;
// 物体着色 + 环境光 + 高光 + 边缘光
return rampColor * _LightColor0 * objColor + ambient * objColor + specular * _SpecColor * _LightColor0 * objColor + rim * objColor;
多色阶卡通着色
这样一个简单的双色阶卡通着色就完成了(当然还可以加上阴影之类的不提),有的时候会需要多色阶的卡通着色,需要多一些步骤。
当然,加上色阶的数量 ToonSteps:
// multi steps
// _RampThreshold 控制光影的比例
float diff = smoothstep(_RampThreshold - diffuse, _RampThreshold + diffuse, diffuse);
// 色阶边缘模糊
float interval = 1 / _ToonSteps;
float level = round(diff * _ToonSteps) / _ToonSteps;
float ramp = level + interval * smoothstep(level - _RampSmooth * interval * 0.5,
level + _RampSmooth * interval * 0.5, diff);
ramp = max(0, ramp);
fixed4 rampColor = lerp(_ShadowColor, _BrightColor, ramp);
当然并不一定效果会很好,右边那个就挺怪的…
传统艺能 Chan小姐,稍微调整了一下。比如脸上是两个色阶其他部分是多色阶,加上高光整体非常油腻,所以去掉了高光。
模型是Unity官方素材Chan
参考资料
- “卡通渲染:从零开始” 写的挺不错的,然而用的是 surface shader…(虽然没差多少)
- “Roystan 的 Toon Shader 教程” 非常赞,主要是双色阶的做法。
- “手机上用的日漫赛璐珞” 这篇用GGX的例子讲Cel Shader蛮不错的,有几个关键点。 比如阴影的色彩查找(用NdotL mul 颜色确实很不可控,把色彩控制交给美术比较靠谱)