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 { 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 { 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
意思是:
剔除正面三角形,只渲染背面。
因为:
- 外扩模型本质是“膨胀版本”
- 如果不剔除正面,会覆盖原模型
- 剔除正面后,只剩外圈
这是描边成立的关键。
为什么要先画描边 Pass
因为:
- 外扩模型比原模型稍大
- 先画黑色轮廓
- 再画原模型覆盖内部
最终只留下边缘。
OutlineWidth 如何控制
1
| pos.xyz += normal * _OutlineWidth;
|
通常范围:
进阶优化
问题一:缩放后描边会变粗
如果物体 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