效果一览
外描边是许多游戏的画面需求,通常大体分为法线外扩和后处理边缘检测两种,法线外扩通常用于特殊需求,如外描边高亮关键物体,选中外描边高亮等,后处理边缘检测画面表现力更强一点,通常用于全屏的风格化描边,如卡通渲染,素描风格画面等(其实我也不清楚,凭感觉应该有这样的使用趋向区别),本篇文章主要讲解法线外扩外描边效果。
法线外扩外描边效果
基本原理
一个shader两个pass,第一个pass绘制法线外扩后的纯色,第二个pass正常绘制
第一个pass绘制法线外扩后的纯色
具体细节
UnityShader代码如下
Shader "Custom/Outline"
{
Properties{
_MainTex("Texture", 2D) = "white"{}
_Diffuse("DiffuseColor", Color) = (1,1,1,1)
_Specular("SpecularColor",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8,256))=32
_OutlineColor("OutlineColor", Color) = (1,0,0,1)
_OutlineLength("OutlineLength", Range(0,1)) = 0.1
}
SubShader
{
//第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
Pass
{
//剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
Cull Front
//深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
//即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
Offset 20,20
//Zwrite Off
CGPROGRAM
#include "UnityCG.cginc"
fixed4 _OutlineColor;
float _OutlineLength;
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata_full v)
{
v2f o;
//在物体空间下,每个顶点沿法线位移,这种描边会造成近大远小的透视问题
//v.vertex.xyz += v.normal * _OutlineLength;
o.pos = UnityObjectToClipPos(v.vertex);
//将法线方向转换到视空间,为接下来转换到投影空间做准备
float3 normalView = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
//将视空间法线xy坐标转换到投影空间,z深度不转换的原因是尽量避免垂直于视平面的顶点位移
//防止描边pass与正常渲染pass的模型交叉
float2 offset = TransformViewToProjection(normalView.xy);
//最终在投影空间进行顶点沿法线位移操作
o.pos.xy += offset * _OutlineLength;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//这个Pass直接输出描边颜色
return _OutlineColor;
}
#pragma vertex vert
#pragma fragment frag
ENDCG
}
//第二个pass利用Blinn-Phong着色模型正常渲染
Pass
{
CGPROGRAM
#include "Lighting.cginc"
fixed4 _Diffuse;
sampler2D _MainTex;
//使用了TRANSFROM_TEX宏就需要定义XXX_ST
float4 _MainTex_ST;
fixed4 _Specular;
float _Gloss;
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float2 uv : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert(appdata_base v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldPos = mul((float3x3)unity_ObjectToWorld, v.vertex);
//通过TRANSFORM_TEX转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos);
fixed3 halfDir = normalize(viewDir + worldLightDir);
fixed3 specular = _Specular * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
fixed3 diffuse =_LightColor0.xyz * _Diffuse * saturate(dot(worldNormal, worldLightDir));
fixed4 color = tex2D(_MainTex, i.uv);
color.rgb = color.rgb * diffuse + ambient;
return color;
}
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
FallBack "Diffuse"
}
其中关于第一个pass中的Offset深度便宜操作有以下解释:
格式为Offset factor , units,默认不设置时是Offset 0 , 0
每一个Fragment的深度值都会增加如下所示的偏移量:
offset = (m * factor) + (r * units)
m是多边形的深度的斜率(在光栅化阶段计算得出)中的最大值。这句话难以理解,你只需知道,一个多边形越是与近裁剪面(near clipping plan)平行,m就越接近0。
r是能产生在窗口坐标系的深度值中可分辨的差异的最小值,r是由具体实现OpenGL的平台指定的一个常量。
一个大于0的offset 会把模型推到离你(摄像机)更远一点的位置,相应地,一个小于0的offset 会把模型拉近。
Offset在这里的作用为防止描边pass与正常渲染pass的模型交叉,例子如下:
设置为Offset 0 , 0即为默认状态时的描边效果,可看到在模型内部,描边pass覆盖部分正常渲染pass
设置为Offset 20 , 20,意味着描边pass距离相机更远,描边集中在模型外边缘,内部少有覆盖正常渲染pass
若描边pass开启Zwrite Off,则会解决pass模型交叉的问题
Pass
{
//剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
Cull Front
//深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
//即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
Offset 0,0
Zwrite Off
......
设置为Offset 0 ,0 , Zwrite On
设置为Offset 0 ,0 , Zwrite Off
可以看到Zwrite Off解决了内部模型交叉问题,同时也带来了一个新特性,多个物体重叠时只会描外边,内部重叠的地方没有描边,适用于多选重叠的物体时高亮提醒的外部描边。
Zwrite Off同时也会带来诸多其他不稳定因数,如下例
在天空盒绘制的地方,外描边消失
通过分析Frame Debug,发现天空盒的绘制顺序在描线物体之后,因为描边没有写入深度而被天空盒覆盖
Unity的geometry类型的渲染顺序是从前往后的,而Transparent类型是从后往前的。天空盒的渲染顺序位于geometry之后,Transparent之前。因此我们把描边pass放在Transparent的渲染顺序就不会被geometry类型遮挡了。 但我有疑问,天空盒不是有默认的渲染顺序background=1000吗?怎么这里会变成geometry之后,Transparent之前?是否是unity为了early-Z而做的内部优化?)
解决方案
在描边shader中更改绘制顺序为Queue=Transparent
SubShader
{
Tags{"Queue"="Transparent"}
//第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
Pass
{......
虽然解决了当前的问题,但关闭深度写入,调整渲染顺序并不是很稳妥的做法,随着工程继续不知道还会出现什么坑,在此不鼓励这么做,对于教程,it just work就OK啦
缺点
法线外面边对于边缘平滑,法线变化不大的模型效果比较好,对于形如立方体这类多边形较明显,法线突变状况比较多的模型下效果较差,会出现描边断裂的情况,如:
立方体描边效果差劲
人物模型的平滑角度阈值设置为0,人物将变成lowpoly风格
lowpoly风格人物描边效果差劲