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

Unity3D Shader 描边效果。

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

前言

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

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

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

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

原理解析

我们先理解一个关键点:

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

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

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

1
pos.xyz += normal * _OutlineWidth;

那么:

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

如果:

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

那么最终效果就是:

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

实现方式(双 Pass)

我们需要两个 Pass:

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

第二个 Pass:绘制正常模型

完整 Shader 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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

1
Cull Front

意思是:

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

因为:

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

这是描边成立的关键。

为什么要先画描边 Pass

因为:

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

最终只留下边缘。

OutlineWidth 如何控制

1
pos.xyz += normal * _OutlineWidth;
  • 值越大 → 描边越粗
  • 值太大 → 会产生穿插

通常范围:

1
0.01 ~ 0.1

进阶优化

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

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

改进方式:

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

1
pos.xyz += v.normal * _OutlineWidth;

适合不考虑世界缩放时。

问题二:远处描边太细

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

1
2
float dist = distance(mul(unity_ObjectToWorld, v.vertex).xyz, _WorldSpaceCameraPos);
pos.xyz += normal * _OutlineWidth * dist * 0.1;

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

优缺点分析

优点

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

缺点

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

适用场景

  • 角色选中描边
  • 卡通渲染(Toon)
  • 交互物体提示
  • 低成本 Outline

Unity3D Shader 顶点法线外扩实现描边效果
http://weikunou.github.io/2026/02/25/unity-shader-vert-normal-outline/
作者
Awake
发布于
2026年2月25日
许可协议