Unity3D Shader 顶点法线外扩实现描边效果

Unity3D Shader 顶点法线外扩实现描边效果

前言

在很多游戏中,我们都见过这样的效果:

  • 被选中的角色有一圈描边
  • 交互物体高亮外轮廓
  • 卡通渲染风格有黑色边线

有一种非常经典、性能也不错的做法:

在顶点着色器中沿法线方向"外扩"模型,再用单色渲染。

原理解析

我们先理解一个关键点:

每个顶点都有一个法线(Normal)。

法线代表这个点"朝向哪里"。

如果我们在顶点阶段做这样一件事:

c 复制代码
pos.xyz += normal * _OutlineWidth;

那么:

  • 模型就会整体向外"膨胀"
  • 看起来像放大了一点

如果:

  • 外扩的模型用黑色渲染
  • 原模型正常渲染
  • 外扩模型先画

那么最终效果就是:

外层黑色边线 + 内层原始模型

实现方式(双 Pass)

我们需要两个 Pass:

第一个 Pass:绘制外扩模型(黑色)

第二个 Pass:绘制正常模型

完整 Shader 实现

c 复制代码
Shader "Custom/NormalOutline"
{
    Properties
    {
        _MainColor ("Main Color", Color) = (1,1,1,1)
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
        _OutlineWidth ("Outline Width", Float) = 0.05
    }

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

        // ========================
        // Pass 1:绘制描边
        // ========================
        Pass
        {
            Cull Front   // 关键:剔除正面,只绘制背面

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            float _OutlineWidth;
            fixed4 _OutlineColor;

            v2f vert(appdata v)
            {
                v2f o;

                float4 pos = v.vertex;

                // 法线转世界空间
                float3 normal = UnityObjectToWorldNormal(v.normal);

                // 沿法线方向外扩
                pos.xyz += normal * _OutlineWidth;

                o.pos = UnityObjectToClipPos(pos);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return _OutlineColor;
            }
            ENDCG
        }

        // ========================
        // Pass 2:绘制原模型
        // ========================
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            fixed4 _MainColor;

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return _MainColor;
            }
            ENDCG
        }
    }
}

效果:

关键点详解

为什么要 Cull Front

c 复制代码
Cull Front

意思是:

剔除正面三角形,只渲染背面。

因为:

  • 外扩模型本质是"膨胀版本"
  • 如果不剔除正面,会覆盖原模型
  • 剔除正面后,只剩外圈

这是描边成立的关键。

为什么要先画描边 Pass

因为:

  • 外扩模型比原模型稍大
  • 先画黑色轮廓
  • 再画原模型覆盖内部

最终只留下边缘。

OutlineWidth 如何控制

c 复制代码
pos.xyz += normal * _OutlineWidth;
  • 值越大 → 描边越粗
  • 值太大 → 会产生穿插

通常范围:

c 复制代码
0.01 ~ 0.1

进阶优化

问题一:缩放后描边会变粗

如果物体 Scale 变大,描边也会变粗。

改进方式:

使用对象空间法线,而不是世界空间:

c 复制代码
pos.xyz += v.normal * _OutlineWidth;

适合不考虑世界缩放时。

问题二:远处描边太细

可以基于摄像机距离做补偿:

c 复制代码
float dist = distance(mul(unity_ObjectToWorld, v.vertex).xyz, _WorldSpaceCameraPos);
pos.xyz += normal * _OutlineWidth * dist * 0.1;

实现远近一致的视觉粗细。

优缺点分析

优点

  • 实现简单
  • 性能较好
  • 不需要后处理
  • 不依赖屏幕空间

缺点

  • 受模型法线影响
  • 锐角处可能断裂
  • 透明物体不适用
  • 多 Pass 会增加 DrawCall

适用场景

  • 角色选中描边
  • 卡通渲染(Toon)
  • 交互物体提示
  • 低成本 Outline
相关推荐
小菱形_2 小时前
【Unity】TimeLine
unity·游戏引擎
小贺儿开发1 天前
Unity3D 自动化物流分拣模拟
运维·科技·unity·自动化·人机交互·传送带·物流分拣
EQ-雪梨蛋花汤1 天前
【3D可视化】基于 Unity 的智慧体育馆三维信息可视化大屏实践
3d·unity·信息可视化
weixin_424294671 天前
Unity 使用Steamworks.NET
unity·游戏引擎
ellis19701 天前
Unity资源管理框架Addressables总结
unity·游戏引擎
yj爆裂鼓手1 天前
unity编辑器下ab包模式下textMeshPro文本不显示材质是紫色的异常,真机无异常的问题
unity·编辑器·材质
EQ-雪梨蛋花汤1 天前
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题
unity·layui·webgl
淡海水3 天前
【节点】[EvaluateTipThickness节点]原理解析与实际应用
unity·游戏引擎·shadergraph·图形·evaluate·thickness
小贺儿开发3 天前
Unity3D 木胎雕刻
科技·unity·人机交互·互动·雕刻