0.本文示例代码地址
1. 极简描边
1.1 思路
使用两个 Pass 来渲染,两个 Pass 的作用分别是
- 把模型放大一定倍数,渲染成纯色
- 正常渲染模型
这样,因为第一个纯色 Pass 把模型放大了,所以第二个正常渲染 Pass 之外能看到一圈纯色的描边,问题在于第一个 Pass 的时候如何把模型放大呢?答案是:
把模型的每个顶点往该顶点的法线方向偏移一定位置
1.2 代码
Shader "Custom_Shader/Outline"
{
Properties
{
_OutlineColor ("Color", Color) = (0, 0, 0, 0)
_OutlineWidth ("OutlineWidth", Range(0, 1)) = 0.1
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
float4 _OutlineColor;
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
// 在模型空间法线外扩
v.vertex.xyz = v.vertex.xyz + v.normal * _OutlineWidth;
o.vertex = UnityObjectToClipPos(v.vertex);
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;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
float4 _OutlineColor;
float _OutlineWidth;
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
关键代码分析:
v2f vert (appdata v)
{
v2f o;
// 在模型空间法线外扩
v.vertex.xyz = v.vertex.xyz += normalize(v.normal) * _OutlineWidth;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
在第一个 Pass 的顶点着色器中来“放大”模型,这里是在模型空间进行放大,你愿意的话完全可以变换到世界空间进行法线外扩。最终渲染效果都是一样的,但这里需要注意的是如果将法线从模型空间变换到世界空间,需要使用变换矩阵 M 的逆转置矩阵来进行变化。
顶点从模型空间变换到世界空间:
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
法线从模型空间变换到世界空间:
o.normal = mul(v.normal, (float3x3)unity_WorldToObject);
还注意到第一个 Pass 有关闭深度写入:
Pass
{
ZWrite Off
为什么第一个 Pass 要关闭 ZWrite?
因为整个模型放大了,面向摄像机的部分会比原始模型更接近摄像机,写入深度会导致第二个 Pass 渲染时面向摄像机部分因为深度测试没通过而没有渲染出来,得不到正确的结果。