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
相关推荐
天人合一peng1 小时前
unity 生成标记根据背景色标记变色
unity·游戏引擎
天人合一peng5 小时前
unity 生成标记根据背景色变色为明显的颜色
unity·游戏引擎
魔士于安6 小时前
Unity 超市总动员 超市收银台 超市货架 超市购物手推车 超市常见商品
游戏·unity·游戏引擎·贴图·模型
CandyU26 小时前
Unity —— 数据持久化
unity·游戏引擎
zh路西法6 小时前
【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用
游戏·unity·游戏引擎
凡情10 小时前
android隐私合规检测
android·unity
小贺儿开发11 小时前
Unity3D 本地 Stable Diffusion 文生图效果演示
人工智能·unity·stable diffusion·文生图·ai绘画·本地化
mxwin1 天前
Unity Shader 半透明物体为什么不能写入深度缓冲?
unity·游戏引擎·shader
晚枫歌F1 天前
三层时间轮的实现
网络·unity·游戏引擎
咸鱼永不翻身1 天前
Lua脚本事件检查工具
unity·lua·工具