之前可能在面剔除中提到过,面剔除可以用来实现描边效果。(以下效果图来自Unity3D ShaderLab开发实战详解)
原理:这是一个最简单的描边,使用面剔除:Cull指令,上图中 ,最左边的球使用的是Cull Front, 中间的使用Cull Back。最右边的球第一个Pass使用了Cull Front并且将球体沿法线挤出一点点,第二个Pass使用Cull Back正常渲染,从而产生了描边效果。下面开始讲一下各种描边。
1.最简单的方式,一个pass讲物体沿法线挤出,形成轮廓。
效果:
代码:
Shader "Tut/Shader/Toon/Outline_1" {
Properties {
_Outline("Outline",range(0,0.2))=0.02
}
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
v.vertex.xyz+=v.normal*_Outline;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=0;
return c;
}
ENDCG
}//end of pass
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
struct v2f {
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.normal=v.normal;
o.lightDir=ObjSpaceLightDir(v.vertex);
o.viewDir=ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N=normalize(i.normal);
float3 viewDir=normalize(i.viewDir);
float diff=dot(N,i.lightDir);
diff=(diff+1)/2;
diff=smoothstep(diff/12,1,diff);
c=_LightColor0*diff;
return c;
}
ENDCG
}
}
}
这个方法有两个问题:1.重叠物体区域没有描边,因为关闭了ZWrite,后面渲染的物体根据ZTest的结果将物体自己渲染输出写入,把轮廓擦掉了;2.轮廓的粗细和相机远近有关,距离越远,轮廓越细;3.有些地方轮廓是间断的,比如上图中cube,相邻两个面的法线方向是分离的,再沿法线挤出去后当然就被分开了。
2.先解决第一和第二个问题,打开ZWrite,第一个问题就解决了,第二个问题我们希望最终的输出是在屏幕上看到的那样挤出来,而不是模型上,不希望考虑镜头的远近,也就是在视空间下挤。
效果:
代码:
Shader "Tut/Shader/Toon/Outline_1x" {
Properties {
_Outline("Out line",range(0,0.1))=0.02
}
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Front
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 norm = mul ((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float2 offset = TransformViewToProjection(norm.xy);
//offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
float4 frag(v2f i):COLOR
{
return 0;
}
ENDCG
}//end of pass
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
sampler2D _MainTex;
struct v2f {
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.normal=v.normal;
o.lightDir=ObjSpaceLightDir(v.vertex);
o.viewDir=ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N=normalize(i.normal);
float3 viewDir=normalize(i.viewDir);
float diff=dot(N,i.lightDir);
diff=(diff+1)/2;
diff=smoothstep(diff/12,1,diff);
c=_LightColor0*diff;
return c;
}
ENDCG
}
}
}
3.接下来解决间断的问题:如果相邻的面法线方向不太一致,挤出来后的方向就会产生断裂。如果不把顶点数据当作位置,而当作是一个方向矢量,则不管它被多少面共享,这些共享顶点的方向是唯一的。这样交接处就不会因为往不同方向挤出而导致断裂了。
效果:
代码:
Shader "Tut/Shader/Toon/Outline_2" {
Properties {
_Outline("Out line",range(0,0.1))=0.02
}
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Front
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
dir = mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
//offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
float4 frag(v2f i):COLOR
{
return 0;
}
ENDCG
}//end of pass
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
sampler2D _MainTex;
struct v2f {
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.normal=v.normal;
o.lightDir=ObjSpaceLightDir(v.vertex);
o.viewDir=ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N=normalize(i.normal);
float3 viewDir=normalize(i.viewDir);
float diff=dot(N,i.lightDir);
diff=(diff+1)/2;
diff=smoothstep(diff/12,1,diff);
c=_LightColor0*diff;
return c;
}
ENDCG
}
}
}
4.上面对第三个问题的解决方法还有点小问题,当物体的模型空间的原点不在几何中心时,就会出现某些地方沿顶点挤出的方向看不到,描边会很不均匀(下图),需要调和法线和顶点方向(其实就是顶点的方向和法线的方向相互拉一拉,靠拢一下)。
解决方法,简单的,比如第一个Pass的顶点shader改为法线和顶点方向的调和:
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
也可以通用一些,试用于复杂图形。通过顶点方向和法线的点积为正或负来判断是指向几何中心还是背离几何中心,并且可以得到指向或背离的程度。
效果:
代码:
Shader "Tut/Shader/Toon/Outline_3x" {
Properties {
_Outline("Out line",range(0,0.1))=0.02
_Factor("Factor",range(1,100))=1
}
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Front
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
float _Factor;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
float D=dot(dir,dir2);
D=(D/_Factor+1)/(1+1/_Factor);
dir=lerp(dir2,dir,D);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
float4 frag(v2f i):COLOR
{
return 0;
}
ENDCG
}//end of pass
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
struct v2f {
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.normal=v.normal;
o.lightDir=ObjSpaceLightDir(v.vertex);
o.viewDir=ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N=normalize(i.normal);
float3 viewDir=normalize(i.viewDir);
float diff=max(0,dot(N,i.lightDir));
diff=(diff+1)/2;
diff=smoothstep(0,1,diff);
c=_LightColor0*diff;
return c;
}
ENDCG
}
}
}
5.上面的方法导致出现一个新问题,cube中间的棱没描出来,解决方法:可以使用两个Pass中和,第一个Pass沿法线方向挤出,这个时候有裂痕了,第二个Pass在裂痕的基础上更多的向顶点方向挤出,这样就在裂痕处形成描边。
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Back
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
float _Factor;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
float4 frag(v2f i):COLOR
{
return float4(1,1,1,1);
}
ENDCG
}//end of pass .1
pass{
Tags{"LightMode"="Always"}
Cull Front
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline2;
float _Factor2;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {//.2
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor2);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline2;
return o;
}
float4 frag(v2f i):COLOR
{
return float4(0,0,0,0);
}
ENDCG
}//end of pass .2
}
_Factor2在第一个系数_Factor的结果上来调整描边。但是现在还缺少一个光照上色的Pass,所以使用混合,最终效果(自行调整参数):
代码:
Shader "Tut/Shader/Toon/Outline_4.1" {
Properties {
_Outline("Out line",range(0,0.1))=0.02
_Outline2("Out line2",range(0,0.1))=0.02
_Factor("Factor",range(0,1))=0.5
_Factor2("Factor",range(0,1))=0.5
}
SubShader {
pass{
Tags{"LightMode"="Always"}
Cull Back
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
float _Factor;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
return o;
}
float4 frag(v2f i):COLOR
{
return float4(1,1,1,1);
}
ENDCG
}//end of pass .1
pass{
Tags{"LightMode"="Always"}
Cull Front
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline2;
float _Factor2;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert (appdata_full v) {//.2
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor2);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline2;
return o;
}
float4 frag(v2f i):COLOR
{
return float4(0,0,0,0);
}
ENDCG
}//end of pass .2
pass{
Tags{"LightMode"="ForwardBase"}
Blend DstColor Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
sampler2D _MainTex;
float _Factor;
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
float3 lightDir:TEXCOORD0;
float3 viewDir:TEXCOORD1;
float3 normal:TEXCOORD2;
};
v2f vert (appdata_full v) {
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
float3 dir=normalize(v.vertex.xyz);
float3 dir2=v.normal;
dir=lerp(dir,dir2,_Factor);
dir= mul ((float3x3)UNITY_MATRIX_IT_MV, dir);
float2 offset = TransformViewToProjection(dir.xy);
offset=normalize(offset);
o.pos.xy += offset * o.pos.z *_Outline;
o.normal=v.normal;
o.lightDir=ObjSpaceLightDir(v.vertex);
o.viewDir=ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N=normalize(i.normal);
float3 viewDir=normalize(i.viewDir);
float diff=max(0,dot(N,i.lightDir));
diff=(diff+1)/2;
diff=smoothstep(0,1,diff);
c=_LightColor0*diff;
return c;
}
ENDCG
}
}
}
补充:通过视线和法线的夹角来判定是否是物体边缘进行描边不稳定,这个方法常用于Rim效果,下节讲Rim。