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

前言
在课程上半场,我们了解了法线是什么
有时候为了调试方便需要把法线画出来,那么怎样把它画出来?把法线画出来,其实还有一个目的——锻炼大家对shader开发和控制的能力
通过法线的可视化,希望大家能够更深入的理解法线是什么东西,它的数值是什么含义?
这都是作为TA同学的基本功
版权声明
本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明
更多学习资源请私信我获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
完整视频请点击观看
法线可以做什么?
下面我们来了解一下法线可以做什么
光照计算

首先法线可以用来进行光照计算,为什么法线可以用来进行光照计算呢?
你想一下平时在台灯下面学习,看到的光线强度是不是跟灯光照射物体表面的角度有关系,所以灯光的照射方向是影响光照强度的第一个因素
第二个因素是物体表面的方向
前面我们讲过法线代表物体表面的方向,所以法线跟灯光方向可以用来进行光照强度的计算,进而把物体表面的光照效果表现出来
那么如何计算物体表面的受光强度呢?我们需要用到一点数学知识,很多同学会觉得学习shader,学习技术美术要掌握很多数学,其实并没有大家想的那么复杂
点乘

比如点乘,因为时间关系,没有办法讲点乘内部的技术实现,这又是另外一节课的知识了,但是我们不理解,或者没有学过点乘的基本知识,不影响学习shader,为什么呢?因为我们只要了解点乘的用法就可以了
如图示左上角的小图,如果灯光的方向跟物体表面的方向是相同的,那这时候我们算出来的点乘值就是1.0,大家记住这个结论
物体表面的方向是朝上的,如果把台灯横着照物体表面,那这时光线会比较弱,用点乘代表光线强度算出来,结果值就是0
我们可以推出来,如果A跟B之间的夹角,随着它逐渐增大,算出来的点层乘值是从1往0跑的

很自然同学们还会想到第三个问题,如果A的方向是朝上的,而B的方向是朝下的,这个时候算出的点乘值是负一,这就表示这两个方向是完全相反的

同样的道理,如果A跟B垂直是0,当它们夹角超过90度越接近于180度,刚好相反,那么它就会从零越接近负一
所以此时可以用点乘进行光照计算
光和法线的光学反应

一般来讲,我们在unity当中或者在自然界当中会有方向光,方向光是一种平行光,它造成在物体表面
但是为了数学计算的方便,一般在程序当中,不使用光线的方向向量,而是使用光线方向的反方向向量跟物体表面的某一个点的法线方向(如图中所画的是它的法线方向),计算夹角的点乘值,用点乘值表示光照强度系数
刚才说了夹角越小,它的光照强度越大,越接近于1.0,夹角越大,它的光照强度越接近于负的1.0

那么我们可以看上面这个图,N表示物体表面的法线,L表示灯光,光照方向相同,点乘值越接近于一
如果法线跟灯光方向垂直,点乘值接近于零
如果法线跟灯光方向方向相反,它的点乘值就是负一
所以就可以用点乘值表示光照强度系数,另外要注意的一点就是光照强度系数不会是负一,为什么呢?
光照强度最小就是0,这也符合现实当中的直觉,如果灯光方向是朝下的,反方向就是朝上的
灯光方向如果朝下,它照在物体的正面是受光的,只要灯光跟物体的夹角超过90度,也就是在物体的背面(我们用绿色表示背面),它的光照强度值就应该是零,是黑色的(黑色表示没有受任何光线),不会出现光照强度负一的情况
那么如何表现光照强度不会是负一呢?
可以用shader当中的一个函数saturate,它的功能是把括号里面的数值,也就是点乘值夹持到零到一之间,虽然N点乘L的结果可能是负一到正一,但是我们可以把它夹持到最小值是零,最大值是一
具体的saturate的内部数学运算,大家可以参考小白的TA之路

要实现刚才我们讲的光照计算,其实方法也很简单,代码一共就几行
首先把顶点的法线转换成每一个像素的法线,然后把O(像素的法线)输入到像素数据里面,每个像素的法线跟灯光方向的反方向进行dot,dot就是大家刚才看到的N点乘L的点(点代表点乘),算出来的结果就是介于负一到正一之间的光照强度
然后再用saturate把它变成零到一之间的数值,因为零到一之间的数值是单色,并不是彩色,彩色是rgb三个分量,所以最后渲染出来的小球就只有黑白灰的亮度值
Quiz:你发现问题了吗?

我们看一下渲染出来的效果,这时我们已经可以看到光照出来的黑白灰的效果了,但大家有没有发现渲染出来的黑白灰效果不对,为什么不对呢?
现在我调整灯光方向,小球受光是会发生变化的,因为灯光只能照亮物体的正面
现在旋转小球,物体表面的受光情况应不应该发生变化?
在现实生活当中,拿着手电筒对着物体照不同的表面位置,它的光照效果受光情况会变化,但如果手电筒一直放在同一个位置,旋转我手电筒照小球,那这时光照应不应该变化呢?
不应该发生变化,所以这个计算结果是错的
如果不掌握图形学的知识,那这么简单的代码,可能检查不出来它的问题在哪里,问题在哪里呢?
在这儿计算的时候,使用的法线是物体表面的法线,随着物体的旋转,它的表面法线的方向始终是绑定在物体上面的,你可以理解成跟着物体去变的
所以小球一转,法线方向就变化,这时算出来的光照就会跟着小球旋转去变,但是这是不对的,我们应该怎么做呢?
不管怎么旋转,只把小球的法线变换到游戏世界里面,就不会随着小球去转了
将法线变换到世界空间

具体该怎么做呢?这个中间牵涉到一些数学运算,这个运算并不复杂
在这儿写一个乘法运算,这个乘法运算把法线通过叫做WorldToObject变换到世界空间,这样就不会随着模型的旋转造成小球的受光情况发生变化,它始终不变

提一个进阶的问题,如果你学过一点shader,那么在面试的时候,初级岗位面试经常会被问到这么一个问题
第一个,英文单词WorldToObject就是表示把世界空间的法线变换到物体自身的法线,但是刚才我说的是要把物体自身的法线变换到游戏世界的发法线
你没有学过shader,那这个地方你可以忽略,但是如果你学过shader,准备面试,一定要注意一下这个问题,为什么在这儿用的是把世界空间变换到对象空间,灯光位置照射世界空间的灯光位置,它应该跟世界空间的法线进行运算,为什么要变化到对象空间?
第二个,我们正常进行法线的空间变换,应该在mul的左边写矩阵,矩阵应该放在逗号的左边,作为第一个参数,把向量放在逗号的右边,作为第二个参数
为什么现在这两个参数的位置刚好是反的?反的算出来的结果是不是会有变化?会有什么变化?那为什么这样反着做?得到的运算结果还是正确的呢?
如果是最近要面试且不太明白,也可以扫一扫左下角的爱丽丝老师进行交流
经过处理以后,不管物体怎么旋转,它的光照始终不会随着小球的旋转而变化,那这个结果就是对的

现在已经把基本的光照效果利用法线算出来了,但是这还不是想要做的最终目标
按照我们现在的做法,必须要添加16个小球,才能够把这16个小球的光照利用小球的法线算出来,那么我们怎样不添加小球,仍然能把小球的光照效果算出来呢?
答案很简单——就是用法线贴图,关键是法线贴图该怎么用?

大家看上图所示是我们要达到的目标,你能不能分出来,左边是添加了小球的真实光照效果还是右边是添加了小球的真实光照效果呢,或者说哪个图像没有添加小球,只是用法线贴图算出来,如果你认为左边可以在讨论区中发一,右边请发二
告诉大家——我也分不出来,因为法线贴图在这样的情况里,就是能做到以假乱真的效果,为什么要用法线贴图而不用真实的小球呢?
在课程开场的时候也讲过,如果每个物体为了表现它的真实性,都要把它的模型添加上去,那模型计算的开销比在一张面片上贴一个法线得到的效果,成本高得多
所以我们要选择低成本的方法,一定是用法线贴图

这是我们刚才要渲染的那幅图像,是使用法线的数据,左边和右边其中有一个是用前面的方法把小球的法线画出来,另外一个是贴图转换成法线数据看到的效果,这样看是没有差别的,但假的东西就是假,所以我们接下来看看怎么能看出它是假的呢?

如上图所示,我们就能看出谁是假的了,正确答案是右边是真的,左边是假的
法线贴图只有一种情况能看出来真假,从物体表面的侧面,能看出来它是否是假的
如何实现?

那么我们再回到刚才的实际效果,要实现以假乱真的光照,我们该怎样做呢?
Step1:生成一张法线贴图

首先第一步,让项目里面的美工把这些小球模型在建模软件,比如说3dmax,maya,houdini,blender,任何一个建模软件里面把3D模型包括小球的模型摆好,摆好以后生成一张图像,如上图右边的图像
这个图像就是左边的模型生成对应的法线图像,它生成的方法是把法线信息加1乘以0.5,形成一张法线值在0~1之间的图像
注意:要确保法线贴图没有采用sRGB模式

这边有个小细节,我们要确保法线图在使用的时候没有勾选sRGB的选项,为什么呢?
这个地方就牵涉到叫做伽马颜色空间和线性颜色空间的问题,具体颜色空间问题目前就不细说了
记住一点,如果图像是一个可见图像,比如角色身上面的衣服,就把sRGB勾上,如果图像是肉眼不可见的图像,比如法线图,遮地贴图等等,这时候就不要勾这个选项了,具体的细节我会在课程里面给大家去解释
Step2:采样法线贴图

刚才我们已经生成了一张法线贴图,接着对法线贴图进行采样
因为我把它的法线值从负一到正一变换到了零到一,因此现在要对它进行反向变换,零到一通过乘以2减1,大家课后可以想一下,就可以把它反向变换成法线值介于负一到正一之间的值
然后把贴图上面的法线值跟灯光方向进行点乘,这样就能算出来一个非常逼真的,接近真实效果的动态光照,但实际上我们并没有使用真实的模型,只是使用了一张法线贴图,这就是我们实现的效果

但这样的效果是不是终级效果呢?
不是的,旋转光照效果没有任何问题,但是如果旋转物体,我特意做了两个对比,是不是很容易区分出来左边是假的,右边是真实的小球

这个真假并不是因为法线贴图本身的技术限制,法线贴图从侧面看是会有一些瑕疵,但是在这儿并不是因为法线贴图的技术限制,而是对法线贴图的使用方式是错误的,错在哪里呢?
就是因为上面这段代码,在这儿通过tex2D这句话对法线贴图读取它的颜色值,然后乘以二加一还原成法线方向向
因为这只是一张图片,所以当图片旋转的时候,从任何角度,只要是采样这张图片某一个相同的像素点,它采样出来的法线值始终是一样的,也就是它不会根据物体方向的变化,让法线进行相应的旋转
因为法线代表物体表面的方向,而物体方向变了,法线方向应该跟着变,但它没有跟着变,所以结果就不正确
那么我们要做的就是当物体旋转的时候,法线贴图的法线要跟着变,换句话说法线贴图是处于它自身的坐标空间,叫做切线空间,所以你要做的就是把处于切线空间的法线值变换到世界空间,才能够正确的跟世界空间的灯光位置进行运算
你可以想象物体在不同空间是不能进行相同运算的,比如米跟厘米能直接运算吗,斤和公斤,克和千克能直接运算吗,是不行的,一定要把它换算到相同的单位才能进行运算
同样道理,比如法线处于切线空间,跟灯光处于世界空间,它们虽然都有数值,但它数值代表的含义是不一样的,就跟刚才讲的单位不一样,要换算一样,这个也要换算,怎么换算?有两个办法
第一个,把灯光换算到切线空间
第二个,把切线空间的法线换算到世界空间
哪种方式更好,我会在vip课程里面教给大家,这是性能优化的要点,也是面试时候的必考点
世界空间法线

还有不能用模型自身法线跟灯光进行运算,为什么呢?
可以看左边跟右边的图,它这个点的法线相对于自己物体表面,方向都是(0,1),也就是朝物体表面方向的
但实际上在游戏世界当中,法线相对于整个游戏世界,是在(1,1)的位置,而右边这个是在(-1,1)的位置,所以我们一定要进行法线的换算,或者进行灯光的换算,必须要换算到同一空间才能进行运算

这个中间其实又牵涉到更多的问题,比如切线空间究竟是什么东西,它是如何构成的,这里面还有很多的技术细节,包括unity也可以帮我们生成物体的切线数据,中间也有技术实现原理,我都会用中学,甚至是小学数学的方法教给大家

包括在进行空间转换运算的时候,中间很多的注意点,都是我们平时照着网上的一顿猛抄,但自己做的时候总是不对的,那你就要学习这些知识点,因为网上抄的只是一个表面,也并没有抄到他的精髓,更不知道人家具体是怎么样做的,这些东西我都会在课程里面教给大家

包括法线贴图的解码中间还有注意点,unity里面有好几种解码方式,比如自己手动写函数来解码,调用unity提供的api接口来进行解码,这两者之间有什么区别,用哪个好,我也会教给大家

回到刚才我们讲的问题,要么把法线贴图里面的法线值转到世界空间,要么就把世界空间的灯光方向转到法线空间,如果把法线转到世界空间,那它运用的公式就应该如图所示那样
最后我们可以看到它得到的效果,现在是基本正确了,转动灯光物体的光照是会跟着动态变化的,但其实这个效果还是有不正确的地方,假设左边这个图是假的,右边是真的,右边跟左边计算的光照刚好是相反的

这中间牵涉到一个问题,法线贴图在生成的时候有一些细节是我们要注意的,如果法线贴图生成的时候没有那么正确,那么最后算出来的效果就跟上面一样是反的
最终效果

最终效果就是随着灯光变化,用法线贴图渲染的物体,不管怎么旋转小球或者旋转灯光,它都是以比较完美的方式呈现它的细节感,看上去就像物体表面就有一些凸起一样,但实际上它是没有的
性能优化:转到切线空间计算光照

还有一些性能优化的技巧,究竟应该在切线空间还是在法线空间里面进行细节运算,我都会教给大家
小结

什么是法线?
unity如何生成法线,生成法线的时候如何让法线的生成方向是正确的
法线要不要单位,根据实质的效果,从正确性来说要单位化,但是从细节效果来说又不用单位化
如何将法线可视化<便于调试>
如何利用法线进行光照计算,其中还讲到了一些点乘的数学知识
法线在空间变换上有什么样的的特殊性
法线贴图的错误用法,就是因为没有考虑到它处在切线空间要进行空间变换
法线贴图的色彩空间,色彩空间用不对,结果就是错的
法线贴图的编码和解码,编码和解码有两种方式,可以自己写,也可以用unity内置函数,这两种方式有什么区别
切线空间是如何构成的
unity如何生成切线?
如何运用这个切线空间正确进行法线贴图计算光照
法线贴图shader的性能优化
都可以关注一下小白的TA之路
进阶


当然效果有没有进阶的空间呢?
也是有的,在最早期的年代,要给物体表面凹凸会使用Bump Mapping,这种技术就是把一张黑白图叠加到物体表面,它的细节看上去不精细
然后有了今天我们讲的法线贴图,但是法线贴图还是在物体表面的侧面能看出来瑕疵
所以我们又会使用叫做Displacement Mapping,叫做置换贴图,或者叫做视差贴图
我们课程里面会教给大家如何使用这种进阶的技巧,Displacement Mapping需要用到一种进阶的技术,它的算法如果你没有接触过,可能需要自己摸索半天,老师会用通俗易懂的方式教给大家

我们如果使用了置换贴图和视差映射的技术,从物体的侧面也能看出来,它的凹凸细节是比较逼真的

大家知道现在基本上unity新项目都是采用可编程的渲染管线,也就是URP渲染管线定制了,所以我们的vip课程里面也会教给大家如何基于URP来写shader,包括如何利用URP定制渲染管线解决很多在内置管线下不会遇到的问题
URP性能一定是高的,但是为了实现性能高,它也付出了很多的成本,项目当中大家会碰到很多的问题,老师也会在课程里面教给大家如何解决

这个是unity的开发者大会上面,里面有个案例,如果使用传统的渲染管线,在处理透贴的时候是有问题的,如果你使用了可编程的管线对效果进行处理,效果就是正确的
前面的问题主要是由于贴图的渲染顺序造成的,这个问题在过去内置管线里面是比较难以解决的,但是在可编程管线里面解决起来更容易,但是作为开发者,你需要掌握更多的URP可编程管线的知识
像我们课程里面也有对应的课程案例,包括体积光效果,也是基于URP做了渲染管线定制,帮助大家理解什么是渲染管线定制,如何在项目中真正用好渲染管线定制,大家如果想要了解我们的课程,包括面试要点,作品要点,大家可以添加文末alice老师的联系方式
写在最后
更多学习资源请私信我获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)
点赞、关注、分享可免费获得配套学习资源
完整视频请点击观看