源码学习网 首页 编程学园 Unity3d教程 查看内容

Unity法线外扩外描边Shader

2019-7-16 22:01| 发布者: opiye| 查看: 82| 评论: 0

摘要: 效果一览外描边是许多游戏的画面需求,通常大体分为法线外扩和后处理边缘检测两种,法线外扩通常用于特殊需求,如外描边高亮关键物体,选中外描边高亮等,后处理边缘检测画面表现力更强一点,通常用于全屏的风格化描 ...
腾讯云服务器秒杀

效果一览

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

法线外扩外描边效果

基本原理

一个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类型遮挡了。(此处解释摘自zhuanlan.zhihu.com/p/50,但我有疑问,天空盒不是有默认的渲染顺序background=1000吗?怎么这里会变成geometry之后,Transparent之前?是否是unity为了early-Z而做的内部优化?)

解决方案

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

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

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

缺点

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

立方体描边效果差劲
人物模型的平滑角度阈值设置为0,人物将变成lowpoly风格
lowpoly风格人物描边效果差劲




来源网址:https://zhuanlan.zhihu.com/p/73534708

鲜花

握手

雷人

路过

鸡蛋
上一篇:光线追踪下一篇:UE4移动端卡通渲染