欢迎光临散文网 会员登陆 & 注册

【深入浅出】法线贴图与切线空间(上)

2023-03-08 09:28 作者:独立游戏人-老雷  | 我要投稿

前言

今天给大家分享的是法线贴图与切线空间深入浅出的介绍

如果大家之前做过一些shader效果,那么很有可能用过法线贴图,但是今天要讲的法线贴图跟大家平时在学习,在使用过程中接触的法线贴图,会有一些不一样的地方

在过去学习法线贴图的时候,更多的是立足于如何去使用它。但是对于它背后的一些技术实现可能是囫囵吞枣的

所以今天给大家分享的就是法线贴图背后的一些技术原理

版权声明

本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明

更多学习资源请私信获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)

点赞、关注、分享可免费获得配套学习资源

完整视频请点击观看

议题


  1. 什么是法线?

  2. 如果模型表面本身是没有定义法线数据的,unity如何生成法线?

  3. 当你去写法线相关的一些功能的时候,过去会把法线进行一个单位化,在进入片元着色器的时候,法线究竟要不要单位化

  4. 有的时候为了调试法线的效果,看它是否正确,我们需要把法线进行可视化,那么如何将法线可视化<便于调试>呢?

  5. 如何利用法线来进行光照计算?大家知道用法线进行光照计算的方式是:用法线跟灯光方向进行点乘得到一个光照强度,那具体该怎么做呢?

  6. 法线在空间变换上,有一定的特殊性,它跟顶点位置、空间变换方式是不一样的,法线在空间变换上的特殊性在哪里?

  7. 法线贴图的错误用法有哪些?

  8. 法线贴图的色彩空间,这个也是大家很容易忽视的一点

  9. 法线贴图的编码和解码

  10. 切线空间的构成

  11. 法线贴图在使用的时候,可能遇到一些错误,这些错误,很多是跟里面进行正确的空间坐标处理有关系的,所以我们会在切线空间里面做很多的运算,那么如何去生成这个物体表面的切线,如何正确的运用切线空间和运用法线贴图来进行光照计算?

  12. 如何正确利用法线贴图计算光照

  13. 法线贴图shader的性能优化

如何不添加小球模型,把小球画出来?



在左边这幅图上面,在一个四边形的平面上放置了一些小球,我们把这些小球按照小球自身的法线来进行图形渲染,得到了一个3D的带小球的面板的图像

但是很多时候,大家可以想象一下,如果场景里面有很多物件,这些物件有的非常小,那么即使是一些非常小的物件,如果要把这些物件刻画的非常精细,它的性能开销也是非常大的

所以我们想一想,有没有一种既可以节省性能,又能把物体刻画的比较精细的方法呢?

大家看右边这个图,右边这个图是在场景里面没有放置任何一个小球的模型,只是贴了一张法线贴图,大家就能看到它渲染出来的效果,看上去就像是场景里面有16个小球一样,非常逼真

这就是我们的今天的文章要解决的问题:

  • 如何不添加小球模型,把这些小球给画出来呢?

什么是法线?

要把这些小球在没有小球模型的情况下渲染出来,首先我们需要用到法线,那么我们来了解一下究竟什么是法线


先从最简单的2D情况看起,假设在场景里面,有两个点,我们可以把这两个点构成一条线,这条线构成了一个平面,由线去分割了这个屏幕空间当中的两个平面


这个时候,我给这条线画一条垂线,这条垂线是跟这个两个点构成的,是互相垂直的

接着我在垂线上面其中一个方向和另外一个方向去计算表示方向的,我们叫做向量,向量的长度值是1,我们把这种长度为1的,代表线段构成的平面的方向的线段,我们就叫它向量


因为跟这条线段垂直的垂线有两条,我们通常约定,这两个方向向量并不都是法线向量,只有代表物体表面正方向的那个向量,叫它法线向量


假设这这个线段构成的平面是上图这样的,假设约定它的正方向是鼠标所在的这个方向,只有这一条垂线长度为1的垂线构成的方向向量为法线向量

这就是法线的基本概念,它代表了一个物体(不管是2D还是3D),它的物体表面的方向

法线可以做什么呢?


首先法线可以用来做物理运算,就比如说大家看在我们的图片上面,可以看到有一个小球,这个小球在物体表面进行弹跳


我们知道一个物体的弹跳,小球的弹可以拆成两个方向的运动

  • 第一个水平方向:是在物体表面的平面的这个方向进行运动

  • 第二个垂直方向:是在物体表面的法线的反方向以及物体表面的法线方向上进行上下的弹跳

  • 同时在水平方向上面进行水平运动

在游戏当中,定义了物体表面的法线以后,就能够让小球在法线的正面进行运动,而不是在这个物体表面的背面

因为我们知道物体表面的垂线有两个方向,但只有一个法线方向,当我定义了法线就不会让物体在表面的背面进行运动


当我知道了物体表面的法线以后,同样我也可以让游戏的主角玛丽奥,在物体表面的正面进行运动,而不是在背面来进行滑动


刚才我们说的是在二维空间中的情况,前面我们说了在二维空间当中两个点构成一个线段,它会有一个法线方向


同理在三维空间当中,我们可以通过三个点定义一个三角形,跟三角形所在的平面,大家看这个白色的平面,垂直的方向向量,我们就叫它法线向量

同样的道理,跟平面垂直的法线方向有两个,只有其中一个代表了这个物体表面的法线方向,那么在unity引擎当中是如何确定的呢?到底是上图中朝上的向量还是朝下的呢?

其实在unity当中,能够根据一定的图形学算法来进行计算,那怎么计算呢?这里面牵涉到一些相对复杂的数学概念,简单来看是这样的:(参照上图)

  • 我们在unity当中可以定义三个点,点a、点b、点c,假设我们的物体是按照这个abc的顺序来定义的

  • 在计算物体表面的法线方向的时候,用a跟b的连线构成一个向量,再用a跟c的连线构成第二个向量,这时候unity内部采用了一种叫做左手定则的方式,什么叫左手定则呢?

  • 大家现在可以把左手伸出来,伸出来以后,除了大拇指以外的另外四根手指,从a,b的方向往a,c的方向去绕

  • 当你的四根手指卷起来以后,把你的大拇指竖起来,这个时候大拇指的方向,一定是朝上的

  • 注意是左手不是右手,如果是右手,那么卷出来的方向应该是朝下的,而unity内部使用的是左手定则

具体的内部实现,大家可以参考我们的unity小白的TA之路

大家再想一想另外一个问题:

  • 如果反过来,在我们确定顶点顺序的时候,假设我把C点确定为第二个点,把B点确定为第三个点,这个时候就是a跟c构成第一个向量,a跟b构成第二个向量

  • 在进行叉乘的时候,再把左手伸出来看一下,如果顶点顺序是acb,这个时候算出来的法线方向就应该是朝下的

  • 这样就明白了unity内部,究竟是怎么去这个计算从而得到每一个三角形的法线方向,也就是三角形的正面方向的,这样我们就明白了unity内部运算的原理是什么了

  • 所以我们学习的时候千万不要浮于表面,在网上面找一个现成的shader效果拿过来抄,那是没有用的

如何求得法线?


刚才我们讲了unity内部的工作原理,但实际上我们在项目里面需不需要自己写程序把模型表面的法线方向一个一个手工算出来呢?

是没有必要的,因为unity在内部已经提前帮我们,当你去导入这个模型的时候,有一个默认值,它的默认值实际上已经提前帮我们把这些东西都算好了

当你选中unity里面的一个几何体的模型,这个模型有一个叫做“Normals“的选项,这个选项有三个值:

  • 第三个选项是叫做None,大家知道在英文里面的意思就是空

  • 另外一个,叫做**Import,**也就是说在美术软件,在dcc建模软件里面,我可以让项目组的美工提前在建模软件里面把法线生成好,然后在unity里面,只要导入这个法线就可以了,这是第二种情况

  • 第三种情况就是我刚才讲的,哪怕你的模型没有法线,我们也可以让unity自己去算,怎么算呢?就是用我刚才讲的左手定则来进行运算,如果你选的是Calculate,也就是自动计算这一项,它就能够把法线在导入模型的时候自动算出来

我再举个例子,比如说像我们在小白的TA之路里面,有一个案例:去绘制原神角色

  • 因为我们知道原神的角色是一个二次元的角色,二次元的角色有个特点,就是角色要进行描边,为了防止角色在描边的时候出现描边断裂,我们就需要在unity当中用代码对模型表面的法线进行重组,这里重组具体怎么重组,我们先不细说

  • 当了解了刚才讲的左手定则以后,就能把三角面的法线算出来,进一步就能够对于每一个顶点上面的进行描边的法线平均化工作,这样就可以让原神的角色不会出现描边断裂的问题

  • 什么是描边断裂?给大家简单讲一下:假设这边有一个立方体,如果我给这个立方体描边,如果你不做特殊处理计算机里面的描边是怎么描的呢

如上图右侧所示,上面描一下,左边这块描一下,右边这块描一下,下边这块描一下,大家看描完以后出现什么问题?

  • 在区域四个角上面就出现了断裂,所以当我们把法线做过特殊处理以后,就不会出现断裂问题了

如何使法线平滑?


在程序当中,我们可以自己去实现物体表面法线的平滑工作,unity其实也为我们准备了相似的选项,这个选项叫做Smoothing angle ,Smoothing就是平滑,angle就是角度

也就是说选项的功能,是按照三角形之间的夹角对法线进行平滑的工作,大家可能看这个选项,会觉得有点陌生,什么叫做按照三角形之间的夹角来进行平滑呢?我们来看下面这个例子


这儿有一个三角形,按照左手定则,我们可以把三角形的法线算出来,但是为了让三角形的法线更加精细,我们不应该是一个三角形一个法线,而是让三角形上面的每一个顶点有一个法线

因为一个物体它可能有很多个三角形构成,并且这些三角形是紧挨着的,也就是说像第三幅图中,两个三角形是共用一条边的,在图形学当中,我们把共用一条边,用红色表示的两个点进行合并,合并成一个顶点

这时候有个问题了,假设左边这个三角形,它的法线朝向是朝左,右边这个三角形,它的法线朝向是朝右。那么这个顶点中间这个顶点,它的法线朝向应该是朝哪里呢?怎么计算呢?

不管用unity、ue,还是用cocos、laya,在图形学里面都是这么做的:

  • 首先,找到左边三角形的法线

  • 然后跟右边三角形的法线进行连接

  • 从顶点的底部连到两个向量加出来的顶部的向量,这个向量的方向就代表了最终的两个三角形共用的顶点上面的法线方向


最后的结果就是什么呢?一个顶点它的法线可能是朝左的,另一个顶点的法线方向可能是朝右的,那么中间这两个点算出来的法线,就是在左边和右边的方向的中间,并且法线只是代表方向,最终我们在进行运算的时候会有一个步骤叫做单位化

  • 单位化是干什么呢?

  • 就是把非常长的向量缩短到长度为一的向量

这就是使法线进行平滑的技巧,但unity是不是把任何的法线都进行平滑呢?

  • 比如说有一个三角面,它是朝左的,另外一个三角面呢,是朝右的,unity默认夹角在多少度以内,比如说夹角在60度以内,才去把这个顶点进行共享,然后计算出来平均化的顶点的法线朝向

  • 如果两个三角面,一个是朝左,一个是朝右,两个夹角是180度,那么unity有没有可能把这样的三角形进行顶点共用、法线共用呢?答案是不可能的!

那么究竟在什么角度把顶点进行共用,unity给我们开放的选择权怎么选择呢?我们回到上面这个图


当你点中一个模型的时候,就能看到这个属性面板,如果完全不需要三角面的顶点共用,就把拉杆拉到最左边,让它的值为零,就是没有任何一个三角形进行法线平均化

通常来讲,我们会把这个值调大,默认值是60,60度角以内的两个三角形,它们会进行法线的平均化

最终做出来效果,就是大家在图片上看到的,左边这个是完全没有法线平均化的,这个球就看上去比较的平面化,右边这个球是利用法线平均化的,它看上去就是一个比较圆滑的小球


这些都是我们平时在开发shader的时候,在使用模型的时候,根本不会注意到的一些点

在这儿再说一个小知识,明明只是调整了物体表面的法线,为什么让物体的颜色看上去变成连续的呢?

实际上我们是利用了光照计算的一些原理,这个原理是什么样的呢?

  • 假设物体表面的法线是比较平滑的,这个时候它算出来的光照强度值也是比较平滑的,当我用比较平滑的光照强度值进行物体表面的着色的时候,画出来的颜色看上去就比较连续,实际上是利用了人眼的视觉欺骗

  • 并不是这个物体本身的模型变平滑了,只是让这个法线的方向变得比较平滑,这样就使物体计算出来的光照看上去是平滑的,就会认为这个模型是平滑的,但本身这个模型是不平滑的

实际上就是我们把物体表面的法线用颜色的方式画出来,法线是一个数值,如下图圆上的红色箭头是法线方向


物体都有X坐标,Y坐标,Z坐标,所以在这儿的法线数值可能X值偏左,偏左就是从原点往X的负方向,它可能是负的0.1

因为它是朝上的,所以它的Y方向可能是0.7,它如果朝向小球的背面,那么Z方向在unity里面就是指向屏幕里面的方向,就是z轴的正方向,就是一个正数,比如说0.2

当然这个数可能并不一定准确,大概的意思是这个样子

那么我们能不能把这些数画在屏幕上面呢?

是可以的,所有法线的方向数值,它的每一个分量,它的数值通通都是介于-1~+1之间的,所以我们要做的,就是个-1~+1的值换算成颜色值

如何可视化模型法线?

但是在计算机里面,颜色值的范围是0~1之间的,要把法线的颜色值画出来,这是TA要干的活儿,我们应该怎么做?

  • 问题很简单,就是如何把在-1~+1间的值变成介于零到一之间的值,我们只要去运用一个小小的计算公式就可以,我们只需要写几行代码就可以了

  • 首先,物体表面有很多的顶点,但我们要画的不是每一个顶点上面的颜色,而是顶点中间的每一个小像素点的颜色,怎么做呢?

  • TA同学在工作的时候,要处理的程序有两个方面:一个是顶点处理程序,另一个是把顶点换算成顶点之间的像素

  • 第一步:顶点上面的法线,在英文里面叫normal,法线传到每一个像素点上面,就像我刚才画的一样,顶点和顶点之间构成像素点

  • 第二步:在像素点上面进行运算,normal的值是-1~+1之间的数值,只要把数值加一,变成0~2

  • 第三步:再乘以0.5,0~2之间的值乘以0.5,就变成了0~1


  • 通过加1乘以0.5,把法线值变成了color,变成了颜色值

  • 上面是基于每个顶点的法线数据,传到像素着色器里面,然后对法线进行加1乘以0.5的操作就得到了最终的颜色值

法线插值

一般来说,unity传到程序当中的法线数据本身就是一个单位化的数据,英文叫做normalized就是单位化的数据,那么我为什么还要在进入每一个像素进行处理的时候,重新对它进行一遍normalize,也就是重新单位化一次呢?

  • 因为在顶点处理阶段,这个顶点的长度也是一,但是我要计算的是每一个像素点的颜色值,这个时候要用法线加一乘以0.5把它转成像素颜色

  • 计算机内部有一个操作叫做光栅化,光栅化操作是干什么呢?

  • 就是这边有个顶点上面的法线,它就把法线构成了一条线,但是问题是如果左边这个法线长度是一个单位,长度是一,右边这个法线,它的长度也是单位一,那么中间在光栅化的时候,通过插值运算出来的结果,就不是一个单位长度了,向量就不能代表法线方向

  • 因为数值不一样,算出来的颜色的结果值是不一样,所以在这里需要进行一个操作,叫做单位化

我举一个现实当中的例子,比如说技术美术同学在工作的时候,如果不能够正确的理解单位化或者忘记单位化,那么这个时候,它渲染出来的图形学效果感觉好像差不多,但是总感觉差了点意思,原因是什么呢?

  • 其实在工作当中,我们往往就是因为忽略了某一些细节,比如说要在光栅化以后进行重新的法线单位化

再告诉大家一个技巧,实际上在很多专业的技术美术同学工作的时候,对于这些法线向量,是不执行单位化这个步骤的

有同学会说,刚才的又要单位化,现在又说不要单位化,到底说的哪一句话是对的呢?


  • 从图形学的严谨性的角度来说,在这里是需要进行normalize单位化这个步骤的,但是任何的数学运算都是有开销的,比如说我要进行单位化,单位化的操作在内部是干什么呢?

  • 一个法线,它有X坐标,有Y坐标,有Z坐标,它要把这个XYZ坐标进行平方,然后加起来,再进行开根号

  • 开根号以后就能计算出来法线的lens,我们记为L

  • 再用X坐标,除以lens,作为单位化以后的X坐标,用Y坐标除以L作为单位化的Y坐标,再用Z坐标除以L作为单位化以后的Z坐标


  • 是不是觉得这个操作需要很多的步骤,先平方再加法,然后开根号最后再除法,一听就觉得很复杂,所以计算机内部算起来也需要很多的运算指令来完成,就会产生开销

  • 是否单位化,从视觉角度来说,很多时候差别并不明显,所以很多的专业TA,技术美术同学,在开发出新学效果的时候,可以不用考虑单位化的科学性,而是从实际效果,从性能的角度来说,可以把normalize这个步骤去掉

如果一个效果做出来,在肉眼上有太明显的差别,那这时候就可以对一些运算步骤做一些省略和优化,在这儿我们就又学到一个新的概念叫法线插值

刚才讲的单位化,包括还有其他的图形学算法,这些公式技巧其实并不难,我都会在图形学的课程里面带大家去手写一遍这些图形学算法,而不是告诉你有这个公式,我会用程序的方式带大家把这些normalize内部怎么实现的手写一遍,让你真正明白图形学内部是如何工作的,感兴趣的小伙伴可以进群交流

回到课程开始的问题:

  • 为了表现模型表面细节,我们需要创建高精度的模型,这个模型可能使用了几万个顶点甚至更多,而顶点越多,渲染的开销越大

  • 那么如何使模型表面保留丰富的法线信息来计算光照、产生丰富细节,同时避免使用过多的顶点呢?

  • 这就需要用到法线贴图和切线空间的知识了,需要了解的小伙伴可以私信我获取进阶视频内容


【深入浅出】法线贴图与切线空间(上)的评论 (共 条)

分享到微博请遵守国家法律