Toon Shader 101

卡通着色入门

Posted by Xjoshua on October 6, 2019

从荒野之息林克的卡通着色中,主要有三个部分:

  • 多色阶着色(荒野之息是双色阶)
  • 高光反射
  • 边缘光

botw

荒野之息的 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
        }
    } 
}

01 标准的兰伯特漫反射模型,显示出来的物体光照只有漫反射和环境光的效果。

加入色阶控制:

// two steps
fixed4 rampColor = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, diffuse);
// 物体着色 + 环境光
return  rampColor * lightCol * _LightColor0 + ambient * objColor;

02

物体着色分成两个色阶。不过阴影处的颜色太深了,需要控制一下。 设置好最亮的颜色和阴影处的颜色,然后把着色映射到区间:

// two steps
fixed4 rampColor = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, diffuse);
// 控制阴影的颜色
rampColor = lerp(_ShadowColor, _BrightColor, rampColor);

03

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;

04

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;

05

多色阶卡通着色

这样一个简单的双色阶卡通着色就完成了(当然还可以加上阴影之类的不提),有的时候会需要多色阶的卡通着色,需要多一些步骤。

当然,加上色阶的数量 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);

06 当然并不一定效果会很好,右边那个就挺怪的…

传统艺能 Chan小姐,稍微调整了一下。比如脸上是两个色阶其他部分是多色阶,加上高光整体非常油腻,所以去掉了高光。

07 模型是Unity官方素材Chan

参考资料

  1. “卡通渲染:从零开始” 写的挺不错的,然而用的是 surface shader…(虽然没差多少)
  2. “Roystan 的 Toon Shader 教程” 非常赞,主要是双色阶的做法。
  3. “手机上用的日漫赛璐珞” 这篇用GGX的例子讲Cel Shader蛮不错的,有几个关键点。 比如阴影的色彩查找(用NdotL mul 颜色确实很不可控,把色彩控制交给美术比较靠谱)