边缘光

本来打算把边缘光和透视效果一起折腾的,忽然发现边缘光可以写的东西也不少,单独开个坑吧,凑凑字数。

菲涅尔

效果图
效果图
最常见的边缘光实现方式之一,通过获取法线和视线的点乘的结果来判断,越靠近模型的边缘,那么法线和视线的夹角就会越接近90度。
示意图
核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
v2f vert (appdata_base v)
{
...
//求法线坐标
o.worldNormal = mul(v.normal,(float3x3)unity_WorldToObject);
//获得世界坐标系下顶点坐标
float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
//获得世界坐标系下视线向量
o.worldViewDir = _WorldSpaceCameraPos.xyz - worldPos;
...
}

fixed4 frag (v2f i) : SV_Target
{
...
//标准化视线向量
float3 worldViewDir = normalize(i.worldViewDir);
//标准化法线向量
fixed3 worldNormal = normalize(i.worldNormal);
//获取 Rim
float rim = 1 - saturate(dot(worldViewDir,worldNormal));
//获取 RimColor
float4 rimColor = _RimColor * pow(rim, 1 / _RimPower) * _RimIntensity;
...
}
  • 注意求坐标的时候叉乘顺序不要错了,不然结果是不对的
  • 法线坐标和视线向量我的参考文章说在 frag 中标准化会影响结果,但是我自己没看出来,可能我的效果比较单一,不过如果你在 vertex 中写下来没影响的话还是在 vertex 里面写,可以节省一些性能。
  • 在 frag 中求 Rim 的时候,用 1 减去的原因是两个向量越接近 90 效果应该越大,但是点乘的结果却越接近 0.
  • pow 的作用是用来平滑的,因为从视觉效果来看边缘光也应该是越接近边缘的地方效果越好,在下图里面,x 轴就是直接得到的 rim 结果。
    坐标轴

深度信息法

深度信息有点类似于之前做过的描边,逻辑就是先将坐标偏移一个距离,然后用原来的坐标和新的坐标相减(或者相加都行,看你想要外描边还是内描边),再用一个阈值来判断原来的坐标是否处于你想要的描边边缘上,最后赋予一个颜色加起来就行。

这里最麻烦的就是如何获取深度值,步骤如下:

  • 先找到屏幕点
  • 向某个方向偏移,并且手动进行齐次除法
  • 获取两个点的深度值
  • 进行线性转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//先计算屏幕点,并且去除屏幕分辨率的影响
float2 screenParams01 = float2(i.vertex.x/_ScreenParams.x,i.vertex.y/_ScreenParams.y);

//向左边偏移
float2 offectSamplePos = screenParams01-float2(_RimOffect/i.clipW,0);

//获取偏移点深度值
float offcetDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePos);
//获取当前点深度值
float trueDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);

//进行线性转换
float linear01EyeOffectDepth = Linear01Depth(offcetDepth);
float linear01EyeTrueDepth = Linear01Depth(trueDepth);

硬边缘效果图用了黑色当原色,比较容易看出效果。

效果图01
效果图02

参考链接