在游戏中,我们都喜欢加一些描边效果,来凸显人物的边缘,提高识别度。美术一般都喜欢加。描边方式一般有两种,一种的模型边缘描边,一种的人物的转折点描边(这种需要用到卷轴)
在游戏中比较常用的就是模型边缘描边了,shader一般是这样
Shader "Outline"
{
//属性
Properties{
_Diffuse("Diffuse", Color) = (1,1,1,1)
_OutlineCol("OutlineCol", Color) = (1,0,0,1)
_OutlineFactor("OutlineFactor", Range(0,1)) = 0.1
_MainTex("Base 2D", 2D) = "white"{}
}
//子着色器
SubShader
{
//描边使用两个Pass,第一个pass沿法线挤出一点,只输出描边的颜色
Pass
{
//剔除正面,只渲染背面,对于大多数模型适用,不过如果需要背面的,就有问题了
Cull Front
CGPROGRAM
#include "UnityCG.cginc"
fixed4 _OutlineCol;
float _OutlineFactor;
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert(appdata_full v)
{
v2f o;
//在vertex阶段,每个顶点按照法线的方向偏移一部分,不过这种会造成近大远小的透视问题
//v.vertex.xyz += v.normal * _OutlineFactor;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//将法线方向转换到视空间
float3 vnormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
//将视空间法线xy坐标转化到投影空间,只有xy需要,z深度不需要了
float2 offset = TransformViewToProjection(vnormal.xy);
//在最终投影阶段输出进行偏移操作
o.pos.xy += offset * _OutlineFactor;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
//这个Pass直接输出描边颜色
return _OutlineCol;
}
//使用vert函数和frag函数
#pragma vertex vert
#pragma fragment frag
ENDCG
}
//正常着色的Pass
Pass
{
...
}
}
//前面的Shader失效的话,使用默认的Diffuse
FallBack "Diffuse"
}
第一个问题:但是,如果这样放到游戏中回有一定的问题
可以看到,一些边缘的地方其实没显示边缘色,导致看起来是断层的。
原因是我们剔除了前面,朝向前方的定点被剔除,所以显示不完整。然后我设置为后,
就比较完美了。然后我一位这样就解决了。
第二个问题:但是后面在主场景中发现一个问题可以看到角色整个黑掉了,开始找原因,首先找到的是,人物身体的mesh多了一份,然后把他移除掉一份,其实还是不行。然后因为我们用了两个pass,两个pass都是transparent的,但在frame debug中看渲染顺序也是正常的,说明和渲染顺序无关。
再后来,发现出现这个问题的情景时两个人物叠加在一起。
由于渲染了front,两个英雄的向前的点会重叠(因为渲染完一个人物的两个pass,再到另一个人物的)
然后我想到的解决办法时,加一个模板,但大于等于0的点就设置为Zero。最终解决了这个问题。
可以看到没有显示黑的一团。
第三个问题:在渲染3d和2d结合的界面,如果我们直接渲染3d(而不是用rt来间接实现)的话会出现看不到描边的问题。这个的原因就是因为ui是会在3d模型之后渲染的,然后我们描边是不回写深度的所以会看不到描边(因为由于没写深度,描边在ui后面)。
因为我们游戏用的是ngui,用uitexture来显示背景,所以会按照ui的方式显示在前面。所以我用了一个quad去实现背景,并且把渲染顺序改为geometry,这样就会显示在后面。
至此就解决了描边的遇到的问题了。
对,是会显示不出来,因为他也是ui。但是要根据实际情况并且消耗情况,制作复杂情况来决定。这个挡板没描边是可以接受的,所以我们用一个比较简单消耗低的方式实现。
当然如果你要完全显示描边,那就可以把rt放到camera的target上,然后用uitexture或quad去装载这个rt,就能完全实现,但这样会对一个相机,并且每帧都要去做这样的操作。具体需要自己权衡。
注意:
1。描边的对象菱角最好不要多,因为描边是通过法线拓开面的方式做的,所以面会往法线上移,所以菱角多可能会出现有些地方空了
2.计算过程如果一方归一化后另一方也要归一化,不然计算会涉及一些不相关的部位。
data.NdotV.x = saturate(dot(normalize(v.normal), normalize(V)));
3.其实这里光照计算不需要用到真实光照,只需要知道一个光照方向就好了,模拟出真实光照感觉。因为如果用真实光,如果你每个场景人物的旋转角度不一致,那么看上去是不一致的,那么这时要调整光照。那么调整光照方向就是最直接的。
float3 lightDirNorm = normalize(_LightDir.xyz);
data.lightDir = lightDirNorm;
之后只需要用这个光照方向去计算边缘光,暗部渲染,高光等就好了,注意如果用到matcap,matcao的高亮部位要跟这个方向一致。
4.做matcap的时候我们会发现摄像机越远他会出现越多的不相关暗部。
这里是因为摄像机角度渲染时mask图涉及的区域可能会超过范围,这里渲染的就会超过一点影响到贴图的其他部位。那么要减少这个影响,就应该根据摄像机的远近来处理uv。减少影响。
(step(uvx1, s.uv_main.x) * step(s.uv_main.x, uvx2)) > 0 ? (_UseMatCap > 0 ? ((mask.a != 0) ? float3(c.rgb * matMaskACapColor * 1.5) : float3(c.rgb * matCapColor * _MatCapColor * 1.5)) : c.rgb) : c.rgb;
(step(uvx1, s.uv_main.x) * step(s.uv_main.x, uvx2))是说uv的x限制在uvx1和uvx2之间。
当超过了这个像素 就用回c.rgb。其他部分都是matcap部分,就不细说了。