Unity3D Shader中的小芝麻(Dot点乘解惑篇)

背景

最近干货发表了很多关于Shader的帖子,介于大环境这么好,我也投入到了Shader中的学习中,在学习中发现一些问题,往往标准的模型映射MVP之流反而没有什么难度,实际上他们到像是过场跑龙套的。进过学习分析,每个shader的原理一般只是有一个核心概念或者核心公式,其中而关于Dot点乘的应用部分,往往让我不能迅速理解,所以这里分析三篇Shader帖子中的关于Dot点乘的应用,彻底掀开她的盖头来。

光照中的点乘应用

本节效果

漫反色中的核心函数

fixed4 diff = albedo * _LightColor0 * max(0, dot(i.worldNormal, worldLight));

当 worldLight 为float3(0, 1, 0),时,主要分析该函数 dot(float3(0, 1, 0), i.normal);

点乘两个向量,我们知道向量归一化以后向量点乘的值是向量的夹角的Cos值(简单理解)

根据图片我们知道,如果两个向量的夹角为0,也就是向量方向相同,那么cos这个值趋近于1。

这里需要注意的一点就是光源的方向是指指向光源的方向向量,而不是光的方向向量。

我们知道指向光源的方向和法向量的夹角不会大于90度,如果出现大于90度说明光源在平面的背面,我们不需要负光的出现,所以需要“受钳制的光照”,这里有两个函数可以选择

一个是:max(0, dot(float3(0, 1, 0), i.normal)); 这个函数比较简单,取最大值,由于cos的最大值是1,所以产生的值是(0,1)

另一个是:saturate(dot(float3(0, 1, 0), i.normal));这个函数内置的饱和值函数,当然这个函数把结果限制在0和1之间

最后一个是高级函数, UnityStandardBRDF导入文件定义了方便的DotClaped函数(实际是根据显卡做了性能选择和封装,详情见本节链接)

另外说一个关于反色光的计算问题,也就是我们初中必背的一个公式,入射角=反色角,这里shaderlab提供了一个内置函数 reflect

float3 reflectionDir = reflect(-lightDir, i.normal);

这里需要注意的这个函数负的 –lightDir 因为这里的参数需要的是光源的方向,且两个参数需要归一化的,具体的延伸阅读见下图:

边缘提取中额点乘应用

本节效果1 轮廓增强

本节效果2 边缘自发光

所以我们 这个viewDir就是世界坐标系中模型指向相机的,和上一节的光照方向是一样的。这样我们在分析下边缘提取的公式。

min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));

1、dot(viewDirection, normalDirection),这个我想大家都明白就是求相机和法向量的余弦值,夹角越小值越趋近1,夹角越大值越趋近于0,也就是值大小与夹角成反比

2、abs(dot(viewDirection, normalDirection)),求绝对值,我们知道当夹角大于90度的时候,cos为负值,也就是摄像头从背面看物体(你以为是透视啊,实际shader有这个能力的)所以这里为什么用的abs而不是上节中的max(有待证明)

3、tex.a / abs(dot(viewDirection, normalDirection)),首先求反,也就是1/x,我们在1中知道dot的值与夹角成反比,再求反的话,负负为正了,该值变成与夹角成正比了,由于又做了绝对值操作,所以求反的值也在(0,1)之间但是趋近于1的时候,说明法线和视线垂直(正交),也就是边缘点。

4、tex.a*这个(0,1)的值,也就是说夹角越大(在边缘)透明度越高,在中心透明度越低,这就自然形成边缘高亮或者轮廓高亮的效果了

5、min(1.0, tex.a / abs(dot(viewDirection, normalDirection)));,由于颜色的值是在(0,1)之间的大于1也没有意义,所以可以用这个min函数限制最大值是1

上代码

  1. void surf (Input IN, inout SurfaceOutput o) {

  2. //该像素的真实颜色值

  3. half4 c = tex2D (_MainTex, IN.uv_MainTex);

  4. //从凹凸贴图中得到该像素的法向量

  5. o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

  6. //得到世界坐标系下的真正法向量(而非凹凸贴图产生的法向量)和雪落

  7. //下相反方向的点乘结果,即两者余弦值,并和_Snow(积雪程度)比较

  8. if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))

  9. //此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于

  10. //lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大,

  11. //积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色

  12. o.Albedo = _SnowColor.rgb;

  13. else

  14. //否则使用物体原先颜色,表示未覆盖积雪

  15. o.Albedo = c.rgb;

  16. o.Alpha = 1;

  17. }

效果很美,代码注释很好,还记的本文的背景嘛,每个shader都有它的灵魂和核心公式,这篇翻译的也不错,但是美中不足,核心公式可读性差了些。我们来分析下,首先落雪的原理是什么,现实中能积雪的是什么地方呢?是一个平台要有面积才能承接上积雪,当然面的方向要与下雪的方向垂直,很少见到墙面上能积雪的一般都是屋顶是这样吧。结合前两节的关于小芝麻Dot的学习,我们知道通过Dot我们可以求得面法线与指定方向的夹角,从而求得面与方向的夹角和边缘位置,这里我们可以把落雪的方向当成光源的方向实际上和漫反色一个道理,“落雪方向的反方向和面法向量夹角越小,说明落雪方向与面垂直,积雪越多“,下面我们看下shader的核心公式,我们分解分析下

if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow))

1、dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz) 翻看_SnowDirection知道是传入变量也确实是落雪反方向(模型指向落雪方向),那么这个dot就是求得法线与该方向的cos值,角度越小值越大,也就是面越垂直值越小,下文中的”落雪方向“都指的是实际落雪的反方向

2、lerp(1,-1,_Snow)是什么1,-1的插值函数,恶补下公式

float lerp(float a, float b, float w) {   return a + w*(b-a);
}
也就是注释给的1-2*_Snow

//此处我们可以看出_Snow参数只是一个插值项,当上述夹角余弦值大于

//lerp(1,-1,_Snow)=1-2*_Snow时,即表示此处积雪覆盖,所以此值越大,

//积雪程度程度越大。此时给覆盖积雪的区域填充雪的颜色

看到这个计算式子,其实是一个斜率为负值的直线方程,也就是该值与_Snow参数 成反比,值越大结果值越小(读到这里估计很多人一头雾水了)

3、dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>lerp(1,-1,_Snow)

按照注释大家理解下这个公式,感觉如何,我第一次度的时候觉得挺绕的,后来仔细想想,问题出在原作者的编码风格上,这里实际上代码规范做的不好,没有遵循单一原则。下面我来分解下

A、不等式左边,dot部分,法线与”落雪方向“夹角越小值越大,符合我们分析的原理,面与”落雪方向“垂直值越大,很好理解,这里与两个向量的夹角成反比,但是面与指定方向成正比,也就是面和落雪方向夹角越大,值越大,正是我们需要的;

B、不等式右边,lerp部分,是什么?不是什么插值计算,最后求得,是一个阈值是一个常量。

那不等式整体的意思是什么呢,当法线与”落雪方向“夹角小与一个值是 就会积雪,反过来好理解当面与落雪方向夹角大于一个值就落雪,好理解吧。

那B中的lerp部分中是什么呢,其实是另一件事情,它是求这个阈值的,当参数越大返回值越小,而阈值越小则允许落雪的角度差越大就是就会落更多的雪,其实也是一个求反,以符合人的惯性思维_Snow越大代表雪越多,恰好不等式右边越小雪量越大,是不是分开更好理解,一行代码只做一件事情。

4、不完美的地方

根据源码分析,我们发现dot没有做负值判断,也就是当发现视线和”落雪方向“大于90度时,会出现负值,实际上这种情况根本就是多余的,物体的内面根本不会出现积雪,至于插值 lerp(1,-1,_Snow)的下降速度采用1,0也应该是没有问题的。

积雪效果的点乘综合应用–积雪厚度变形

上代码

  1. void vert (inout appdata_full v) {

  2. //将_SnowDirection转化到模型的局部坐标系下

  3. float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

  4. if(dot(v.normal, sn.xyz) >= lerp(1,-1, (_Snow*2)/3))

  5. {

  6. v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;

  7. }

  8. }

这里我引下原文的原理说明,很简单了

首先我们传给vert函数一个参数appdata_full v,参数的类型为appdata_full(Unity内置类型),该类型包含了纹理坐标,法向量,顶点位置,以及切线信息。如果你还需要使用其他的数据类型,你可以使用自定义的输入结构体作为pixel函数的第二个参数传递额外的信息 — 目前我们不需要这样做。

_SnowDirection使用的是世界坐标系,但是我们需要的其实是模型局部坐标系下的_SnowDirection。所以我们需要先将_SnowDirection转化到模型的局部坐标系下。而我们只需要将_SnowDirection乘以Unity内置矩阵 – UNITY_MATRIX_IT_MV(IT表示Inverse Transpose逆转置矩阵,MV表示 ModelView矩阵,该矩阵表示是ModelView的逆转置矩阵)。

现在我们得到了该顶点的法向量(vert函数应该是对每个vertex调用一次,相对于surf函数对每个pixel调用一次)。我们仍然像上面做积雪效果时那样,将转换坐标系空间后的雪落下相反方向和模型局部坐标系下的法向量进行点乘,得到的结果仍然和一个插值比较。不过此时插值项不再是_Snow,而是_Snow*2/3,这表示只有那些接近雪落下方向的区域才会增加雪的厚度,更符合自然现象。

而这些通过测试的区域,沿着(sn.xyz+v.normal)方向进行加厚,也就是将其顶点沿此方向伸展一定距离。注意到增厚的程度取决于_SnowDepth和_Snow,而增厚的方向是由物体法向和雪落的方向综合作用的,这也符合自然现象。

原理用一句话概括,就是通过dot与阈值判断,加大面顶点的高度,以获得加厚的效果,注意这里应用的向量的加法。

总结
本篇是本人学习shader中dot应用的一篇总结贴,我自己总结的一句话每个shader中都有一个核心原理和公式,关于dot的应用无非是求边缘和求非边缘?

学习游戏开发,进入高薪行业!

点击「阅读原文」报名免费课程

号主说

    如果你想详细学习游戏开发,我为你准备了非常详细的教程和学习资料,在本公众号后台回复「教程」,可直接获取下载地址。

  • 《Unity游戏设计与实现-南梦宫》后台回复「设计」下载,

  • 《3D数学基础:图形与游戏开发》后台回复「数学」下载

  • 《游戏改变世界》后台回复回复「改变」下载

  • 《Unity官方案例精讲》后台回复「案例」下载

    更多资料、更多问题请后台详聊小新新。

注:baidu网盘文件总是被删,我重新生成了链接,你们试试,再不行给我后台留言吧,我也很无奈啊~~

Unity3D游戏开发主程,入门游戏开发必备!

↑↑长按二维码关注我