Unity Toon Shader 卡通着色器(一):卡通着色

一直对非真实感渲染 (Non-Photorealistic Rendering) 很感兴趣,正好发现某社出的新游戏中可以选择真实质感或卡通质感,所以想试试在 Unity 里实现一下卡通着色器。

卡通渲染最关键的特征包括不同于真实感渲染的艺术化光影效果轮廓描边。光影效果即是指将物体受光照的颜色从多色阶降到低色阶,减少颜色的丰富程度。本篇即讨论如何卡通着色,实现该光影效果。

Rendering

* 本文主要参考 Unity Assets Store 中的 Toony Colors Pro 2 ,模型也来自该工具包。着色器全部使用 Surface Shader 实现。

Github 仓库地址: github.com/Sorumi/UnityToonShader
博文原文:sorumi.xyz/posts/unity-toon-shader/


首先搭一下基本的着色器框架,在 Surface Shader 中自定义光照模型 LightingToon,编译指令中排除多余的渲染路径通道,减少最终生成 shader 的体积。

Properties
{
    _Color ("Color", Color) = (1, 1, 1, 1)
    _MainTex ("Main Texture", 2D) = "white" { }
}

SubShader
{
    Tags { "RenderType" = "Opaque" }

    CGPROGRAM

    #pragma surface surf Toon addshadow fullforwardshadows exclude_path:deferred exclude_path:prepass
    #pragma target 3.0

    fixed4 _Color;
    sampler2D _MainTex;

    struct Input
    {
        float2 uv_MainTex;
        float3 viewDir;
    };

    inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
    {
        half3 normalDir = normalize(s.Normal);
        float ndl = max(0, dot(normalDir, lightDir));

        fixed3 lightColor = _LightColor0.rgb;

        fixed4 color;
        fixed3 diffuse = s.Albedo * lightColor * ndl * atten;

        color.rgb = diffuse;
        color.a = s.Alpha;
        return color;
    }

    void surf(Input IN, inout SurfaceOutput o)
    {
        fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);
        o.Albedo = mainTex.rgb * _Color.rgb;

        o.Alpha = mainTex.a * _Color.a;
    }

    ENDCG
}

这是非卡通渲染只有环境光照 (ambient) 和漫反射 (diffuse) 的效果。

No Toon


简化颜色

实现原理是把 diffuse 漫反射颜色简化成对比较明显的几个色阶,首先尝试一下降到 2 阶色阶。Diffuse 模型中,法线方向向量与光线方向向量的点积控制着漫反射的强度。

float ndl = max(0, dot(normalDir, lightDir));

设置属性:RampThreshold 色阶阈值, ndl>=threshold n d l >= t h r e s h o l d ramp=1 r a m p = 1 ndl<threshold n d l < t h r e s h o l d ramp=0 r a m p = 0 。但是这样会导致分界线十分明显,所以再增加属性:RampSmooth 色阶间平滑度。使用 smoothstep 平滑函数,根据 RampSmooth 对色阶之间进行过渡。

fixed3 ramp = smoothstep(_RampThreshold - _RampSmooth * 0.5, _RampThreshold + _RampSmooth * 0.5, ndl);
ramp *= atten;
...
fixed3 diffuse = s.Albedo * lightColor * ramp;

Toon Ramp

可以看到光影对比很明显了,但是这个阴影也太丑了,所以直接用颜色叠加作为阴影和高光。设置属性 HColor 高光颜色和 SColor 阴影颜色。

_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
...
fixed3 diffuse = s.Albedo * lightColor * rampColor;

Toon Color
画面一下子就变得干净了,看起来舒服多啦~


增加镜面高光和边缘光

之后再做一些增强画面效果的工作,首先设置镜面光照的相关属性:SpecColor 高光颜色、SpecSmooth 高光色阶的平滑度、Shininess 镜面反射度。

surf 着色器里,使用纹理的 alpha 通道作为光泽度 Gloss 。

void surf(Input IN, inout SurfaceOutput o)
{
    ...     
    o.Specular = _Shininess;
    o.Gloss = mainTex.a;
}
inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    half3 halfDir = normalize(lightDir + viewDir);
    ...
    float ndh = max(0, dot(normalDir, halfDir));
    ...            
    float spec = pow(ndh, s.Specular * 128.0) * s.Gloss;
    spec *= atten;
    spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
    ...
    fixed3 specular = _SpecColor.rgb * lightColor * spec;

    color.rgb = diffuse + specular;
    color.a = s.Alpha;
    return color;
}

设置边缘光的属性:RimColor 边缘光颜色、RimThreshold 边缘光阈值、RimSmooth 边缘光色阶的平滑度。法线方向与视线方向夹角越小,与光线方向夹角越大,则边缘光强度越强。

inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    ...
    float ndv = max(0, dot(normalDir, viewDir));
    ...
    float rim = (1.0 - ndv) * ndl;
    rim *= atten;
    rim = smoothstep(_RimThreshold - _RimSmooth * 0.5, _RimThreshold + _RimSmooth * 0.5, rim);
    ...
    fixed3 rimColor = _RimColor.rgb * lightColor * _RimColor.a * rim;

    color.rgb = diffuse + specular + rimColor;
    color.a = s.Alpha;
    return color;
}          

Toon SpecularAndRim


阴影纹理

对于模型不同部分,单纯用颜色来做阴影可能缺少层次感,可以考虑使用纹理,对不同部分添加不同的阴影颜色。这里需要自定义 SurfaceOutput ,添加 Shadow 保存纹理采样的颜色。

void surf(Input IN, inout SurfaceOutputCustom o)
{
    ...
    fixed4 shadowTex = tex2D(_ShadowTex, IN.uv_MainTex);
    o.Shadow = shadowTex.rgb;
    ...
}

inline fixed4 LightingToon(SurfaceOutputCustom s, half3 lightDir, half3 viewDir, half atten)
{
    ...
    s.Albedo = lerp(s.Shadow, s.Albedo, ramp);
    ...
}


多阶色阶

一般,多阶色阶是由美术制作一维色彩表的纹理,对其进行采样获得颜色值。现在来考虑一下用程序来实现。

同样使用 RampThreshold 控制光影的比例。

float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);

增加属性 ToonSteps 表示色阶层数。

float ramp = floor(diff * _ToonSteps) / _ToonSteps;

色阶之间需要根据 RampSmooth 平滑过渡,首先来看一下一直使用的 smoothstep 函数。

smoothstep(float min, float max, float t);

根据 t , min 和 max ,返回在 0 到 1之间的一个数,类似于插值函数 lerp 。但是 lerp 是线性,而 smoothstep 在min和 max 处增长缓慢,中间增长较快。

如果 ToonSteps = 5 ,则 ramp 关于 diff 的函数图像为

Ramp

float interval = 1 / _ToonSteps;
float level = round(diff * _ToonSteps) / _ToonSteps;
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *= atten;

但是当 RampSmooth = 1 ,即完全平滑,图像应该是线性的,但是函数图像如下图

Ramp

为了修正这一点,暂时的做法是加了一个判断

float linearstep(float min, float max, float t)
{
    return saturate((t - min) / (max - min));
}

inline fixed4 LightingToon(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
    ...
    if (_RampSmooth == 1)
    {
        ramp = interval * linearstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
    }
    else
    {
        ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
    }
    ...
}

效果:
Toon Multisteps


参考链接

Toony Colors Pro 2
卡通渲染及其相关技术总结
Unity3d shader之卡通着色Toon Shading

### 创建 Hash 类型数据结构 在 Redis 中,`Hash` 数据结构允许存储字段和之间的映射关系。这非常适合用来表示对象属性或其他关联数组形式的数据。 要创建并操作 `Hash` 结构,可以使用如下命令: #### 使用 HSET 命令 通过 `HSET` 可以为哈希表中的某个字段赋。如果给定的字段已经存在于哈希表中,则更新其对应的;如果不存在则会新增此条目。 ```bash HSET myhash field1 "Hello" ``` 这条命令会在名为 `myhash` 的哈希表里设置一个叫作 `field1` 的字段,并将其设为 `"Hello"`[^2]。 #### 获取 Hash 字段 为了获取之前设定好的字段,可利用 `HGET` 来读取特定字段的内容: ```bash HGET myhash field1 ``` 上述指令将会返回先前存入 `field1` 的字符串 `"Hello"`。 #### 批量处理多个字段 对于一次性向同一个哈希表添加多组不同的键对的情况,推荐采用更高效的批量写入方式——即一次调用 `HMSET` 或者多次执行 `HSET` 操作来完成相同目的。 ```bash HMSET user001 name "Alice" age 30 city "Beijing" ``` 这段代码片段展示了如何在一个叫做 `user001` 的哈希表里面同时插入三个不同名称的字段及其对应数。 #### 查询整个 Hash 表内容 当想要查看某哈希表内的全部记录时,可以通过下面这个命令实现: ```bash HGETALL myhash ``` 它将以列表的形式返回所有的字段名以及它们各自的
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值