游戏中点乘的运用

原文:

http://www.manew.com/forum.php?mod=viewthread&tid=107084&page=1#pid1568321

https://www.sohu.com/a/167651842_667928

另外几篇叉乘和点乘的

https://zhuanlan.zhihu.com/p/89046275

https://blog.csdn.net/qiaoquan3/article/details/70194685

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

光照中的点乘应用

本节效果

原文Shader详解 ,http://gad.qq.com/program/translateview/7173932

漫反色中的核心函数

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 轮廓增强

出处 http://www.manew.com/thread-102911-1-1.html [zhang273162308] Unity&Shader基础篇—轮廓增强

本节效果2 边缘自发光

出处2 http://blog.csdn.net/jk823394954/article/details/48983621 Unity Shader 表面着色器边缘光(Rim Lighting)二

效果1分析:

获取边缘的核心函数如下,又看到了我们提到的向量点乘

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

return float4(col, newOpacity);

这里我们先看下 viewDirection是怎么求得

output.viewDir = normalize(_WorldSpaceCameraPos- mul(modelMatrix, input.vertex).xyz);

根据向量的减法我们知道两个向量相减得到的向量的方向由减数指向被减数,如下图所示:

所以我们 这个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

效果分析2:

核心公式

half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal)); 

//赋值自发光颜色信息

o.Emission = _RimColor.rgb * pow (rim, _RimPower); 

1、dot (normalize(IN.viewDir), o.Normal)和效果1一样,也就是视线与法线夹角成反比,而我们知道夹角正交(垂直)的地方是边缘,那么1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));  相当于再次求反,负负为正,那么就变成夹角越大值越大,也就是越是边缘越趋近1,自然实现了自发光效果了。

积雪效果的点乘综合应用--积雪实现

效果图

详细实现请参考 http://www.cnblogs.com/polobymulberry/p/4316683.html 

【译】Unity3D Shader 新手教程(2/6) —— 积雪Shader

上代码

  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的应用无非是求边缘和求非边缘?

在上一篇Unity3d Shader中的小芝麻(Dot点乘解惑篇)的文尾巴,我留了一个引子,“关于dot的应用无非是求边缘和求非边缘?”,

确实Dot在shader中的应用主要是求边缘或者非边缘(也就是面),但是并不是全部,本文的目的是结合向量点积的定义,收集和总结向量点乘Dot(点积)在Unity(game)中的应用。下面看一下向量点积的定义

不用说太多的数学知识,我们把定义分为这三种吧,1、几何意义,2、代数意义,3、矩阵乘法(向量投影)相关。下面我们就用实际中的例子来继续解释这三种定义在游戏开发中的应用。

实现

a.b = x1x2+y1y2

代数意义,实际上如果数学一般的同学,看到这个数学式子,应该是没有什么感觉的我也是如此,后来在不断学习中发现,这么小的一个式子应用竟然如此广泛

A、计算向量长度和平方(模的平方)

如果说你对a.b = x1x2+y1y2 不敏感的话,如果dot中的参数相同也就是 a.a = x1x1+y1y2(勾股定理?),是的向量长度的平方,这个的一个应用是在点光源的衰减因子计算上(与到光源的距离平方成反比)

B、实现灰度公式的快速计算

计算灰度在游戏中是一种常用的图像操作,比如人物死后的画面,不能点击的按钮,死人的头像等等。求灰度的公式是什么呢?

对于彩色转灰度,有一个很著名的心理学公式:

Gray = R*0.299 + G*0.587 + B*0.114

看到这个式子,可能大家都笑了,这次我们对于算式开始敏感了,这就不是点积吗?我们看下例子中的Shader代码

效果如下:

代码很简单,这里就不详细说了

补充实际上灰度化也可以使用颜色平均值,这种简便高效的方式

几何意义,实际上一篇中应用的也是几何意义,求边缘的依据也是归一化向量夹角的cos值

A、Dot正负值应用,判断主角与目标的前后位置,

我们补充一个常用的dot几何意义的补充,根据Dot值,获得目标的位置信息,通过正负值,判断目标前后位置(负值在求边缘中被我们舍弃的部分),文字不好描述上图和代码很清晰了

判断敌我方位

1.判断目标在自己的前后方位可以使用下面的方法:

Vector3.Dot(transform.forward, target.position-transform.position)

返回值为正时,目标在自己的前方,反之在自己的后方

2.判断目标在机子的左右方位可以使用下面的方法:

Vector3.Cross(transform.forward, target.position-transform.position).y

返回值为正时,目标在自己的右方,反之在自己的左方

补充计算目标左右方的代码参考一下代码

public static bool RotationDirection(Vector3 currentDir, Vector3 previousDir) {

if (Vector3.Cross(currentDir, previousDir).z > 0) {

//顺时针 return true; } else {

//逆时针 return false;

}

}

作者:影子丢了

链接:http://www.jianshu.com/p/6bd8623a825e

來源:简书

B、求向量夹角

我想通过Dot几何意义的求向量夹角,可能是我们最熟悉的了,具体就是应用高中的反余弦函数大家都懂

void OnGUI ()

{

//点积的返回值

float c = Vector3.Dot (a, b);

//向量a,b的夹角,得到的值为弧度,我们将其转换为角度,便于查看!

float angle = Mathf.Acos (Vector3.Dot (a.normalized, b.normalized)) * Mathf.Rad2Deg;

GUILayout.Label ("向量a,b的点积为:" + c);

GUILayout.Label ("向量a,b的夹角为:" + angle);

//叉积的返回值

Vector3 e = Vector3.Cross (a, b);

Vector3 d = Vector3.Cross (b, a);

//向量a,b的夹角,得到的值为弧度,我们将其转换为角度,便于查看!

angle = Mathf.Asin (Vector3.Distance (Vector3.zero, Vector3.Cross (a.normalized, b.normalized))) * Mathf.Rad2Deg;

GUILayout.Label ("向量axb为:" + e);

GUILayout.Label ("向量bxa为:" + d);

GUILayout.Label ("向量a,b的夹角为:" + angle);

}

矩阵乘法相关(向量投影)

实际也代数意义相关,在向量的旋转计算中我们引入了旋转矩阵,根据背景中列出的3部分,我们可以用点积来进行矩阵运算的式子分解

如算式敏感的同学的旋转矩阵的应用

这里我们反的看(c是cos,sin是sin)

uv = float2(uv.x*c - uv.y*s, uv.x*s + uv.y*c);

尼玛,我算式敏感了,这是什么?这不是点积嘛,然后我们得出

uv = float2(dot(uv, float2(c, -s)), dot(uv, float2(s, c)));

我的数学天赋有上升了一个层次根据“a·b=a^T*b,这里的a^T指示矩阵a的转置”然后我们得出了

漂亮的效果图

关于如何通过点积获得的旋转,也就是

uv = float2(dot(uv, float2(c, -s)), dot(uv, float2(s, c)));

总结

以上两篇文中提到知识点,是我关于Dot向量点乘的在图形图像(game)应用的总结,以我羸弱的数学知识来说实属不易,最后慨叹一下点积 a.b=|a||b|cos(a,b)这个小小的数学公式竟然有这么大的威力!希望以上知识对你有用

发布了11 篇原创文章 · 获赞 8 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/a1047120490/article/details/104925253
今日推荐