unity蓝色描边 unity描边效果_unity蓝色描边


效果一览

外描边是许多游戏的画面需求,通常大体分为法线外扩和后处理边缘检测两种,法线外扩通常用于特殊需求,如外描边高亮关键物体,选中外描边高亮等,后处理边缘检测画面表现力更强一点,通常用于全屏的风格化描边,如卡通渲染,素描风格画面等(其实我也不清楚,凭感觉应该有这样的使用趋向区别),本篇文章主要讲解法线外扩外描边效果。


unity蓝色描边 unity描边效果_unity 3d物体描边效果_02

法线外扩外描边效果


基本原理

一个shader两个pass,第一个pass绘制法线外扩后的纯色,第二个pass正常绘制


unity蓝色描边 unity描边效果_unity蓝色描边_03

第一个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的模型交叉,例子如下:


unity蓝色描边 unity描边效果_unity 天空盒_04

设置为Offset 0 , 0即为默认状态时的描边效果,可看到在模型内部,描边pass覆盖部分正常渲染pass

unity蓝色描边 unity描边效果_unity 天空盒_05

设置为Offset 20 , 20,意味着描边pass距离相机更远,描边集中在模型外边缘,内部少有覆盖正常渲染pass

若描边pass开启Zwrite Off,则会解决pass模型交叉的问题


Pass
		{
			//剔除正面,只渲染背面,防止描边pass与正常渲染pass的模型交叉
			Cull Front
			//深度偏移操作,两个参数的数值越大,深度测试时该pass渲染的片元将获得比原先更大的深度值
			//即距离相机更远,更容易被正常渲染的pass覆盖,防止描边pass与正常渲染pass的模型交叉
			Offset 0,0
			Zwrite Off 
         ......


unity蓝色描边 unity描边效果_unity 天空盒_06

设置为Offset 0 ,0 , Zwrite On

unity蓝色描边 unity描边效果_unity 3d物体描边效果_07

设置为Offset 0 ,0 , Zwrite Off

可以看到Zwrite Off解决了内部模型交叉问题,同时也带来了一个新特性,多个物体重叠时只会描外边,内部重叠的地方没有描边,适用于多选重叠的物体时高亮提醒的外部描边。

Zwrite Off同时也会带来诸多其他不稳定因数,如下例


unity蓝色描边 unity描边效果_unity 3d物体描边效果_08

在天空盒绘制的地方,外描边消失

通过分析Frame Debug,发现天空盒的绘制顺序在描线物体之后,因为描边没有写入深度而被天空盒覆盖


unity蓝色描边 unity描边效果_unity 天空盒_09


unity蓝色描边 unity描边效果_unity蓝色描边_10


Unity的geometry类型的渲染顺序是从前往后的,而Transparent类型是从后往前的。天空盒的渲染顺序位于geometry之后,Transparent之前。因此我们把描边pass放在Transparent的渲染顺序就不会被geometry类型遮挡了。 但我有疑问,天空盒不是有默认的渲染顺序background=1000吗?怎么这里会变成geometry之后,Transparent之前?是否是unity为了early-Z而做的内部优化?)

解决方案

在描边shader中更改绘制顺序为Queue=Transparent


SubShader
	{
		Tags{"Queue"="Transparent"}
		//第一个pass,各顶点沿法线向外位移指定距离,只输出描边的纯颜色
		Pass
		{......


unity蓝色描边 unity描边效果_unity蓝色描边_11


虽然解决了当前的问题,但关闭深度写入,调整渲染顺序并不是很稳妥的做法,随着工程继续不知道还会出现什么坑,在此不鼓励这么做,对于教程,it just work就OK啦

缺点

法线外面边对于边缘平滑,法线变化不大的模型效果比较好,对于形如立方体这类多边形较明显,法线突变状况比较多的模型下效果较差,会出现描边断裂的情况,如:


unity蓝色描边 unity描边效果_unity 3d物体描边效果_12

立方体描边效果差劲

unity蓝色描边 unity描边效果_unity 天空盒_13

人物模型的平滑角度阈值设置为0,人物将变成lowpoly风格

unity蓝色描边 unity描边效果_unity 3d物体描边效果_14

lowpoly风格人物描边效果差劲