【Unity Shader URP】简易卡通着色(Simple Toon)实战教程

文章目录

    • [0. 效果预览](#0. 效果预览)
    • [1. 原理简述](#1. 原理简述)
    • [2. 功能点](#2. 功能点)
    • [3. 完整 Shader(可直接用)](#3. 完整 Shader(可直接用))
    • [4. 使用方法](#4. 使用方法)
    • [5. 参数说明](#5. 参数说明)
    • [6. 变体与扩展](#6. 变体与扩展)
      • [6.1 二色阶简化版(只要明/暗)](#6.1 二色阶简化版(只要明/暗))
      • [6.2 Ramp 贴图替代 Step(更灵活)](#6.2 Ramp 贴图替代 Step(更灵活))
      • [6.3 屏幕空间等宽描边](#6.3 屏幕空间等宽描边)
    • [7. 常见问题](#7. 常见问题)
    • [8. 性能建议](#8. 性能建议)

0. 效果预览

简易卡通着色(Toon / Cel Shading)是非真实感渲染(NPR)的入门效果:用 step 把连续光照硬切成 2-3 个色阶,再加一个背面扩张的描边 Pass,就能得到经典的手绘动画/赛璐珞风格。《塞尔达:旷野之息》《罪恶装备》《原神》的渲染都以此为基础。


1. 原理简述

卡通着色的本质:把 Lambert 光照的连续灰度用 step/smoothstep 量化为有限色阶(明/暗或明/中/暗),再用第二个 Pass 沿法线方向扩张背面顶点形成描边。

色阶硬切:

hlsl 复制代码
float NdotL = dot(normalWS, lightDirWS);
float halfLambert = NdotL * 0.5 + 0.5;
// 二分硬切:亮面 / 暗面
float toon = step(_ShadowThreshold, halfLambert);
half3 color = lerp(_ShadowColor.rgb, _LitColor.rgb, toon);

step(_ShadowThreshold, halfLambert) 在阈值处一刀切:大于阈值 = 亮面色,小于 = 暗面色。smoothstep 可以让切割边缘有一点柔和过渡,避免锯齿。

描边原理:

hlsl 复制代码
// 第二个 Pass:只渲染背面,沿法线方向扩张顶点
Cull Front  // 剔除正面,只画背面
v.positionOS.xyz += v.normalOS * _OutlineWidth;

背面顶点沿法线外扩一小段距离,形成比正面大一圈的"壳",颜色设为纯黑/深色,就是描边。正面 Pass 正常渲染盖在上面,只露出背面扩张的边缘部分。


2. 功能点

  • 三色阶硬切:Half-Lambert 在两个阈值处硬切为暗/中/亮三段色阶
  • Smoothstep 柔化:可调边缘柔化宽度,避免硬切锯齿
  • 三色独立可调:亮面/中间/暗面颜色各自控制,暗面偏冷+亮面偏暖=经典冷暖对比
  • 法线外扩描边:第二个 Pass 背面扩张,渲染纯色描边
  • 描边宽度可调:Inspector 实时调节描边粗细
  • 主贴图支持:保留模型纹理细节
  • GPU Instancing:支持多实例渲染

3. 完整 Shader(可直接用)

hlsl 复制代码
Shader "Custom/SimpleToon_URP"
{
    Properties
    {
        // 主贴图
        _BaseMap ("Base Map", 2D) = "white" {}

        // ===== 卡通光照参数(三色阶:暗 / 中 / 亮) =====
        // 亮面颜色
        _LitColor ("Lit Color", Color) = (1, 0.95, 0.9, 1)
        // 中间色(半影区域)
        _MidColor ("Mid Color", Color) = (0.7, 0.65, 0.7, 1)
        // 暗面颜色(建议偏冷色)
        _ShadowColor ("Shadow Color", Color) = (0.4, 0.35, 0.5, 1)
        // 暗→中 分界阈值
        _ShadowThreshold ("Shadow Threshold", Range(0, 1)) = 0.35
        // 中→亮 分界阈值
        _MidThreshold ("Mid Threshold", Range(0, 1)) = 0.6
        // 边缘柔化宽度(0=硬切,0.05=微柔,越大越柔和)
        _ShadowSmoothness ("Shadow Smoothness", Range(0, 0.1)) = 0.02

        // ===== 描边参数 =====
        // 描边宽度
        _OutlineWidth ("Outline Width", Range(0, 0.05)) = 0.01
        // 描边颜色
        _OutlineColor ("Outline Color", Color) = (0.1, 0.1, 0.1, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalRenderPipeline"
            "Queue" = "Geometry"
            "RenderType" = "Opaque"
        }

        // =============================================================
        // Pass 1:描边(背面外扩)
        // =============================================================
        Pass
        {
            Name "Outline"
            Tags { "LightMode" = "SRPDefaultUnlit" }

            Cull Front      // 剔除正面,只渲染背面
            ZWrite On

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            float _OutlineWidth;
            float4 _OutlineColor;

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                // 沿法线方向外扩顶点
                float3 expandedPos = v.positionOS.xyz + normalize(v.normalOS) * _OutlineWidth;
                o.positionHCS = TransformObjectToHClip(expandedPos);
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                return (half4)_OutlineColor;
            }
            ENDHLSL
        }

        // =============================================================
        // Pass 2:卡通光照(正面渲染)
        // =============================================================
        Pass
        {
            Name "ToonLit"
            Tags { "LightMode" = "UniversalForward" }

            Cull Back
            ZWrite On
            Blend Off

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            // 贴图声明
            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);

            // 材质属性
            float4 _BaseMap_ST;
            float4 _LitColor;
            float4 _MidColor;
            float4 _ShadowColor;
            float  _ShadowThreshold;
            float  _MidThreshold;
            float  _ShadowSmoothness;

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv          : TEXCOORD0;
                float3 normalWS    : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

                o.positionHCS = TransformObjectToHClip(v.positionOS.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _BaseMap);
                o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(i);

                // 1) 采样主贴图
                half4 baseCol = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv);

                // 2) 计算 Half-Lambert
                Light mainLight = GetMainLight();
                float3 normalWS = normalize(i.normalWS);
                float NdotL = dot(normalWS, normalize(mainLight.direction));
                float halfLambert = NdotL * 0.5 + 0.5;

                // 3) 三色阶硬切(smoothstep 做微柔化)
                // 暗→中 过渡
                float shadow = smoothstep(
                    _ShadowThreshold - _ShadowSmoothness,
                    _ShadowThreshold + _ShadowSmoothness,
                    halfLambert
                );
                // 中→亮 过渡
                float mid = smoothstep(
                    _MidThreshold - _ShadowSmoothness,
                    _MidThreshold + _ShadowSmoothness,
                    halfLambert
                );

                // 4) 三段插值:暗 → 中 → 亮
                half3 toonColor = lerp(_ShadowColor.rgb, _MidColor.rgb, shadow);
                toonColor = lerp(toonColor, _LitColor.rgb, mid);

                // 5) 主贴图叠乘 + 光源颜色
                half3 finalColor = baseCol.rgb * toonColor * mainLight.color;

                return half4(finalColor, baseCol.a);
            }
            ENDHLSL
        }
    }
}

4. 使用方法

  1. 在 Unity 项目的 Assets/Shaders/ 下新建文件 SimpleToon_URP.shader,粘贴上方完整代码。

  2. 新建材质(Create → Material),Shader 选择 Custom/SimpleToon_URP

  3. 将材质赋给场景中的模型(角色模型效果最好,球体也能展示)。

  4. 在 Inspector 中调节参数:

    经典冷暖对比

    • Lit Color = 暖白 #FFF2E6
    • Mid Color = 灰紫 #B3A6B3
    • Shadow Color = 冷紫 #665980
    • Shadow Threshold = 0.35, Mid Threshold = 0.6
    • Shadow Smoothness = 0.02
    • Outline Width = 0.01, Outline Color = 深紫黑 #261A33

    橙红活力风格

    • Lit Color = 暖杏 #FFE0B2
    • Mid Color = 橙红 #FF7043
    • Shadow Color = 深红棕 #BF360C
    • Outline Color = #4E1A08

    青蓝冰感风格

    • Lit Color = 冰白 #E0F7FA
    • Mid Color = 青蓝 #26C6DA
    • Shadow Color = 深青 #00695C
    • Outline Color = #0A2E26

    粉紫糖果风格

    • Lit Color = 浅粉 #FCE4EC
    • Mid Color = 玫粉 #EC407A
    • Shadow Color = 深玫红 #880E4F
    • Outline Color = #3C0522

    翠绿自然风格

    • Lit Color = 嫩绿白 #F1F8E9
    • Mid Color = 翠绿 #66BB6A
    • Shadow Color = 深绿 #2E7D32
    • Outline Color = #1B3D1F
  5. 确保场景中有一个 Directional Light,调整光照方向观察明暗分界线的移动。


5. 参数说明

参数 类型 范围/默认值 说明
_BaseMap 2D white 模型主贴图
_LitColor Color (1,0.95,0.9,1) 亮面颜色
_MidColor Color (0.7,0.65,0.7,1) 中间色(半影区域)
_ShadowColor Color (0.4,0.35,0.5,1) 暗面颜色(建议偏冷色)
_ShadowThreshold Range(0,1) 0.35 暗→中 分界阈值
_MidThreshold Range(0,1) 0.6 中→亮 分界阈值
_ShadowSmoothness Range(0,0.1) 0.02 边缘柔化宽度:0=硬切,越大越柔和
_OutlineWidth Range(0,0.05) 0.01 描边宽度(模型空间单位)
_OutlineColor Color (0.1,0.1,0.1,1) 描边颜色

6. 变体与扩展

6.1 二色阶简化版(只要明/暗)

如果不需要中间色,去掉 _MidColor_MidThreshold,只用一个 step:

hlsl 复制代码
float toon = smoothstep(
    _ShadowThreshold - _ShadowSmoothness,
    _ShadowThreshold + _ShadowSmoothness,
    halfLambert
);
half3 toonColor = lerp(_ShadowColor.rgb, _LitColor.rgb, toon);

更简洁,适合极简卡通风格或性能敏感场景。

6.2 Ramp 贴图替代 Step(更灵活)

用一张 1D 渐变贴图控制色阶过渡,和文章 24(Ramp Shading)原理相同:

hlsl 复制代码
// 替换 step 色阶计算
float2 rampUV = float2(halfLambert, 0.5);
half3 toonColor = SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, rampUV).rgb;

Ramp 贴图里画几个色阶就有几段硬切,比代码更直观、美术更可控。

6.3 屏幕空间等宽描边

当前的法线外扩描边有一个问题:离摄像机远的模型描边变细,近的变粗。如果需要屏幕空间等宽描边:

hlsl 复制代码
// 在 Outline Pass 的 vert 中,改为裁剪空间偏移
float4 clipPos = TransformObjectToHClip(v.positionOS.xyz);
float3 clipNormal = mul((float3x3)UNITY_MATRIX_VP,
                        TransformObjectToWorldNormal(v.normalOS));
// 在裁剪空间沿法线偏移,除以 w 保证屏幕空间等宽
clipPos.xy += normalize(clipNormal.xy) * _OutlineWidth * clipPos.w * 2.0
              / _ScreenParams.xy;
o.positionHCS = clipPos;

7. 常见问题

Q: 描边在某些角度消失或断裂?

A: 法线外扩描边依赖平滑法线。如果模型有硬边(Hard Edge),法线在接缝处不连续,描边会断开。解决方案:在建模软件中烘焙平滑法线到顶点色或额外 UV 通道,Shader 中用平滑法线做外扩。

Q: 描边粗细不均匀,尤其在模型细长部分?

A: 法线外扩在曲率变化大的地方会不均匀。简单解决:降低 _OutlineWidth。高级解决:用屏幕空间等宽描边(见变体 6.3)。

Q: 明暗分界线有明显锯齿?

A: 调大 _ShadowSmoothness(如 0.03~0.05),让 smoothstep 在分界处产生一个窄的柔化过渡带。完全的 step 硬切在低分辨率下容易出锯齿。

Q: 想要暗面偏冷、亮面偏暖的色调?

A: 这正是 _LitColor_ShadowColor 的用途。亮面设暖白 (1, 0.95, 0.9),暗面设冷紫 (0.4, 0.35, 0.5),冷暖对比是卡通渲染的经典技巧。

Q: 描边 Pass 在 URP 中不显示?

A: 检查描边 Pass 的 LightMode 标签。URP 默认只执行特定 LightMode 的 Pass。本文使用 SRPDefaultUnlit,如果不生效,在 URP Renderer 的 Renderer Features 中确认该 LightMode 没被过滤掉。或者改用 UsePass / Render Objects Feature 手动添加。


8. 性能建议

  • 双 Pass 开销:描边需要额外一个 Pass,顶点数翻倍。对大多数场景影响不大,但大量卡通角色时注意 Draw Call。
  • 描边 Pass 极轻量:描边的 frag 只输出纯色,几乎无片元开销。瓶颈在顶点处理,低模角色(< 10K 顶点)完全不需要担心。
  • 色阶计算极低 :一个 smoothstep + 一个 lerp,比标准 PBR 光照省一个数量级的计算量。
  • 合批注意 :双 Pass Shader 会让 SRP Batcher 合批效率降低。如果同场景大量使用,考虑把描边合并到单 Pass(在 frag 中用 fwidth 检测边缘,但视觉效果不如双 Pass 干净)。
  • 移动端友好:除了双 Pass 的额外 Draw Call,计算量比 PBR 低很多,移动端表现良好。
相关推荐
UXbot2 小时前
如何用 AI 快速生成完整的移动端 UI 界面:从描述到交付的实操教程
前端·ui·交互·ai编程·原型模式
魔士于安2 小时前
unity 骷髅人 连招 武器 刀光 扭曲空气
游戏·unity·游戏引擎·贴图·模型
ai_coder_ai3 小时前
自动化脚本ui编程之下拉列表框控件
ui·autojs·自动化脚本·冰狐智能辅助·easyclick
@Demi3 小时前
Cursor 配置 MasterGo MCP 还原UI设计稿
ui·cursor·mastergo·mcp
洛阳吕工4 小时前
从 micro-ROS 到 px4_ros2:ROS2 无人机集成开发实战指南
游戏引擎·无人机·cocos2d
风酥糖5 小时前
Godot游戏练习01-第29节-游戏导出
游戏·游戏引擎·godot
戴西软件5 小时前
戴西CAxWorks.VPG车辆工程仿真软件|假人+座椅双调整 汽车仿真效率直接拉满
java·开发语言·人工智能·python·算法·ui·汽车
瑞瑞小安5 小时前
Unity功能篇:文本框随文字内容动态调整
ui·unity
想你依然心痛5 小时前
HarmonyOS 6(API 23)悬浮导航与沉浸光感实战:打造下一代玻璃拟态UI体验
ui·华为·harmonyos·悬浮导航·沉浸光感