在之前的项目中,我从网上抓了一个用菲涅尔反射写的边缘光shader。经过自己的魔改之后,加上了法线等用在后续的项目中。现在我们回过头来探寻一下菲涅尔效应是如何用来实现边缘光的。

参数

我们有三个至关重要的参数:边缘光的颜色、边缘光强度和强度系数。

  • 边缘光颜色
    这个没什么好说的,就是控制边缘光的颜色。但因为是,所以当颜色调整为黑色时不论强度多少都是看不到效果的。
  • 边缘光强度
    这个值可以控制菲涅尔影响范围的大小,这个值越大,效果上越边缘化(上:强度为1.5;下:强度为4)。

  • 边缘光强度系数
    这个值控制菲涅尔反射的反射强度,值越大,反射越亮,反之越暗(上:强度为1.5;下:强度为12)。代码里的注释有错,因为原先的代码的强度系数在内部被原作者定死了。

片元着色器

边缘检测

这一步是实现边缘光效果的核心。
首先我们来理清一下思路,如何进行边缘检测?其实很简单的一种方式就是通过视线方向和片元的法线方向点乘进行判断。当我们差不多正视一个片元的时候,夹角通常都是小于45°的。而片元越靠近边缘,与视线的夹角越大,最终达到视线相切的90°,再大就看不到了。
所以我们对视线和片元法线归一化之后进行点乘,可以得到:

  • 视线与法线相切,点乘的结果为0
  • 视线与法线重合,点乘的结果为1
  • 视线与法线的夹角在(0°, 45°),点乘的结果在(0, 1)范围内

于是我们得到一个信息:值越小,片元越靠近视线观察角度的边缘。我们可以对这个结果进行求反(用1减去值就好),得到更符合逻辑的结果。

菲涅尔着色

我们已经有了求反过的点乘信息NdotV,接下来我们就需要把它展现在材质球上。我们可以定义一个叫fresnel的变量来简单的将NdotV和控制强度的变量相乘,再乘上边缘光的颜色:

1
2
3
float fresnel = NdotV * _InSideRimIntensity;
float3 Emissive = _InSideRimColor.rgb * fresnel;
return fixed4(Emissive + diffuse + ambient, 1.0);

我们已经可以得到一个初步的效果。但是不幸的是,不管我们怎么调整强度,反射环的面积似乎总是太大了,并且有一种雾蒙蒙的感觉:

在另一篇文章(UE4 Materials 101:材质混合)中我们介绍了用指数提升对比度、缩小范围的方法。因此,我们可以引入一个变量来控制边缘光的强弱(并不是控制光强,而是控制了最大影响范围)。我们可以把代码改写成:

1
float fresnel = pow(NdotV,_InSideRimPower) * _InSideRimIntensity;

这样,我们就得到了不错的边缘光效果了。如果想在这个基础上加上边缘光的“呼吸”效果,可以引入sin(time.z)对光强进行调整。不过需要小心颜色超出1的情况,所以最好加一个max函数进行限制,或者自己拟好合适的三角函数曲线。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fixed4 frag (v2f i) : SV_Target
{
const fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _MainColor.rgb;
// 环境光
const fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
i.wold_normal = normalize(i.wold_normal);
// 获取世界空间的视角方向
float3 worldViewDir = normalize(_WorldSpaceCameraPos.xyz - i.vertexWorld.xyz);
// 获取世界空间的光照方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 漫反射
const fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(i.wold_normal, worldLightDir));

/*核心步骤*/
// 计算法线方向和视角方向点积,约靠近边缘夹角越大,值约小,那就是会越在圆球中间约亮,越边缘约暗
half NdotV = max(0, dot(i.wold_normal, worldViewDir));
// 求反
NdotV = 1.0 - NdotV;
float fresnel = pow(NdotV, _InSideRimPower) * _InSideRimIntensity;
// 内边缘光自发光颜色
float3 Emissive = _InSideRimColor.rgb * fresnel;

return fixed4(Emissive + diffuse + ambient,1.0);
}