Unity法线贴图原理理解(为什么存在切线空间?存的值是什么?)
- 写在前面
- 1、为什么用法线贴图?
- 2、用什么存法线?
- 3、法线向量为什么存在切线空间?法线贴图存得是什么?
- 4、法线贴图为什么会偏蓝?
- 5、如何生成法线贴图?
- 6、怎样用法线贴图计算光照?
- 写在后面
写在前面
最近写Shader使用到了法线贴图,一直没搞懂为什么法线贴图是存在切线空间的?以及法线贴图里到底存的是什么?
看来了网上很多博客以及《Unity Shader入门精要》都没太明白。
我当时很疑惑的点是:法线贴图存储的是模型在切线空间下的法线方向。而切线空间z轴是顶点的法线方向,x轴是顶点的切线方向,y轴由法线和切线叉积的副切线。法线贴图到底存的是什么呢?切线空间都把法线拿去当z轴了,还不所有法线都是(0,0,1)了嘛
最终在https://www.gxlsystem.com/hulianwang-1859689.html中得到了比较合理的解释。
下面是真的上面的引用再加上个人的理解,若有错误欢迎讨论。
1、为什么用法线贴图?
精细的模型虽然光照的效果很好,但是凹凸面太多,导致光照计算量太大。而粗模的凹凸面太少,导致光照效果不好。因为光照效果是受模型法线方向影响的,精模凹凸面多->面的法线方向越多样->光照效果好,粗模凹凸面少->面的法线方向多样少->光照效果不好->导致看起来没有凹凸感。
所以人们想到,能不能把高模的法线存起来用在低模上,从而让低模实现类似高模的光照效果。所以发明了法线贴图用来存高模的法线信息。
2、用什么存法线?
法线向量的3个分量xyz正好对应图片的三通道rgb,所以想到用图片去存法线信息,所以这张存了法线信息的图片就叫法线贴图。
3、法线向量为什么存在切线空间?法线贴图存得是什么?
法线用什么坐标系去存呢?
有三种选择方式:世界坐标系、模型坐标系、切线坐标系
世界坐标系:
法线向量相对于世界原点,那么法线向量与模型本身位置,如果模型换个位置,或则换个场景,就出出错,所以不行。
模型坐标系:
法线相对于模型原点,那么如果模型不动,但某个三角形网格变了,面上的法线向量本应该变了。但是模型原点坐标没变,法线向量是相对于模型原点的,所以法线实际是不会变,这就有问题了。并且由于模型空间下的法线向量是相对于这个模型原点的,不同模型的模型坐标系是不一样的,所以也导致这些模型空间下的法线只能应用于这个模型,换一个模型就不行了。
切线坐标系:
既然世界坐标和模型坐标都不太好,那么人们就想能不能让法线依赖于向量既不依赖于场景又不依赖于模型,而是依赖于模型上的面呢?当面变的时候,法线跟着变。从而让法线始终保持处于这个面的上方。人们为了实现这个目标,提出了切线空间。
坐标系定义:对于粗模 (注意是粗模!!!) 的每个顶点,它都有自己的切线空间,这个切线空间的原点就是该原点本身,而z轴是顶点的法线方向,x轴是顶点的切线方向,y轴由法线和切线叉积而得,称为副切线(bitangent)。
切线坐标系储存的值:
储存的是精模法线向量从模型坐标系转到粗模的切线坐标系的向量。也就是用粗模的切线空间储存精模的法线。
这样,由于每个表面顶点都有自己的切线空间,这个切线空间是以粗模顶点的法线方向为z轴,顶点的切线方向为x轴,副切线为y轴,储存的是精模的法线向量。因此这个精模的法线是相对当前粗模该顶点的切线空间的法线,这里我们假设值为(0,0,1),即精模法线与粗模法线重合(因为粗模法线是z轴嘛)。如果粗模发生变化,这时粗模顶点法线和切线变化,那么这个切线空间变了。此时的(0,0,1)依然代表这个变化后的粗模的顶点法线方向,也就是说你只要知道变化后的粗模的法线方向,那么对应的精模的法线也就是这个法线,那么应用在粗模的光照效果依然是精模的光照效果。
这里我画一个示意图,图上的红绿蓝坐标系就是粗模的切线空间。这里我们假设来自于精模的四个四边形变成粗模的一个四边形。我画的粉色框就是精模的四边形,这时这个四边形顶点的法线就是精模法线向量,然后把这个坐标系放到粗模切线坐标系,此时这个法线向量的值就是法线贴图的值。
4、法线贴图为什么会偏蓝?
因为法线向量各个分量xyz取值区间是[-1,1],而rgb颜色分量范围是0-1,所以需要映射一下:rgb = (xyz + 1) / 2。
上面提到了法线贴图存的值是精模法线向量从模型坐标系转到粗模的切线坐标系的向量。比如说粗模的三角形数量相对于粗模来说没有减少,也就是粗模和精模是同一个模型。这时因为切线空间的z轴是粗模的法向量,由于假设粗模和精模是同一个模型法线是完全相同的,也就是精模的所有法线向量就是z轴,即值为(0,0,1)。这时的法线贴图所有值都是rgb=(0.5,0.5,1),也就淡蓝色。因此只要粗模相对于精模三角形数量和为没有太大变化,那法线贴图大多数颜色就是(0.5,0.5,1)淡蓝色。
5、如何生成法线贴图?
这个问题也就是再说,如何让精模模型空间下的法线转到粗模切线空间下。也就是我们需要一个变化矩阵让精模法线经过变化矩阵变化的粗模切线空间。
这里直接给结论:从一个空间T变换到空间N的变换矩阵,就是T空间的三个基向量xyz在N空间下的表示。
结论来自https://zhuanlan.zhihu.com/p/361417740
因此从切线空间->模型空间:相当于切线空间中三个基坐标(TBN,T为切线、B为副切线、N为法线)在模型空间三个基坐标上的投影(XYZ)。因此变换矩阵为
同理从模型空间->切线空间是切线空间->模型空间的转置,因为是逆向操作
这样我们就得到了从模型空间->切线空间变换矩阵。再用模型空间的法线左乘这个变换矩阵就能得到切线空间的法线了。
注意:因为粗模一个三角面需要对应多个精模的三角面,因此多个法线向量就可以共享一个低模切线坐标系。
6、怎样用法线贴图计算光照?
因为法线贴图是切线空间的,但是光照是在世界空间计算的,因此要计算光照有两种方式。法一:把光照方向和视角法线转到切线空间去计算。法二:把法线从切线空间转到世界空间去计算。
这里我们主要讲法二,把法线贴图中法线转到世界空间去计算光照。
首先我们就要获得从切线空间->世界空间的变换矩阵
切线空间->世界空间:使用法线贴图,如果光照在世界空间中计算,就需要把法线从切线空间转换到世界空间。Matrix = 切线空间三分量在世界空间的表示。
矩阵再乘上切线空间下的法线,获得世界空间法线。
对应shader的代码,o.tangent.w是副切线的方向性的。
//粗模顶点法线
float3 worldNormal = mul((float3x3)unity_ObjectToWorld,o.normal);
//粗模顶点切线
float3 worldTangent = mul((float3x3)unity_ObjectToWorld,o.tangent.xyz);
//粗模顶点副切线
float3 worldBinormal = cross(worldNormal,worldTangent)*o.tangent.w;
//切线空间->世界空间的变换矩阵
r.matrixRow1 = float3(worldTangent.x,worldBinormal.x,worldNormal.x);
r.matrixRow2 = float3(worldTangent.y,worldBinormal.y,worldNormal.y);
r.matrixRow3 = float3(worldTangent.z,worldBinormal.z,worldNormal.z);
有了切线空间->世界空间的变换矩阵,我们就只需要把法线贴图的法线向量左乘这个矩阵就能得到世界空间下的法线向量。主要要反映射一下,因为贴图是rgb[0,1]而法线范围是[-1,1]。
//对法线纹理进行采样,取出其值,这是在切线空间的法线
fixed4 bumpColor = tex2D(_BumpTexture,o.uvNormalTexture);
//解出法线纹理,这是对法线纹理的采样结果的一个反映射操作, normal.xy = packednormal.xy * 2 - 1;
fixed3 bump = UnpackNormal(bumpColor);
bump *= _BumpScale;
//得到副切线
bump.z = sqrt(1 - max(0, dot(bump.xy, bump.xy)));
//法线向量从切线空间转到世界空间
bump = fixed3(
dot(o.matrixRow1,bump),
dot(o.matrixRow2,bump),
dot(o.matrixRow3,bump));
这样我的就得到了法线贴图,然后用这张法线贴图去做漫反射光照即可。
fixed3 diffuseColor = _LightColor0.rgb*mainTextureColor.rgb*max(0,dot(normalize(bump),normalize(_WorldSpaceLightPos0.xyz)));
完整代码
见https://blog.csdn.net/iiiiiiimp/article/details/125505828中的Phong光照模型(顶点片元Shader、表面体Shader)
写在后面
看来一天的博客,才有点弄明白了。由此可见《深海》的图形学技术功底。大家去看看《深海》吧!博主当时哭得好惨!听说12亿才回本!!!