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]Inspector各种写法(持续更新中)
java·unity·游戏引擎
毕竟秋山澪2 天前
unity Skill接入TraeAI操作步骤
unity·游戏引擎
XR-AI-JK2 天前
01-VR开发如何配置和搭建基础环境
unity·vr·vr基础教程·vr教程·vr实战教程·vr节奏游戏·unityvr教程
派葛穆2 天前
Unity-生成预制体1
unity
WarPigs3 天前
Unity CG着色器实战
unity·着色器
废嘉在线抓狂.3 天前
TimeLine如何自定义轨道
unity·c#·对话系统
ellis19703 天前
Unity资源管理框架Addressables[六] 内存管理
unity
派葛穆3 天前
Unity-鼠标悬停改变物体层级
unity·游戏引擎
小贺儿开发3 天前
Unity3D 爆炸图案例演示
unity·产品·urp·机械拆装·爆炸图·零件·效果设计
Yasin Chen4 天前
Unity TMP_SDF 分析(二)数据来源2
unity·游戏引擎