计算法线需要一张高度图,只需要求出u方向上的高度函数

计算法线需要一张高度图,只需要求出u方向上的高度函数

本文主要讲解从纹理生成法线贴图的基本方法,并在Unity中实现和测试。

初步知识

法线贴图和基本图形知识,基本矢量和极限知识。

高度图或灰度图

二维纹理有两个维度u和v,但实际上,高度(h)可以算作第三个维度。 有了高度,二维纹理可以想象成三维物体。

unity地形贴图_法线凹凸贴图_unity 生成法线贴图

让我们首先考虑只有 u 方向的情况。 如图,A和B是纹理中的两个点音效,uv坐标分别为(0, 0)和(1, 0)。 上面的黑线代表该点对应的高度。 显然,只需要某点的高度函数在u方向的切线,就可以得到与其垂直的法线。 v方向也是如此。 也就是说,如果有纹理的高度信息,那么就可以计算出纹理中每个像素点的法线。

所以计算法线需要一张高度图,它代表纹理中每个点对应的高度。

但实际上并不需要在每个纹理像素上都找出各自在uv方向的法线。 只需要求出高度函数在uv方向的切线,然后做叉积,就可以算出对应的法线了。

如果没有高度图,也可以用灰度图代替。 灰度图是rgb三种颜色分量的加权平均。 提取灰度值的算法有很多种。 这里介绍一种比较常用的基于人类感知的灰度。 价值提取公式。

颜色.r * 0.2126 + 颜色.g * 0.7152 + 颜色.b * 0.0722

这个公式来源于人眼对不同颜色的不同敏感度。 这里不用太担心,直接用提取出来的灰度值作为高度值即可。

计算方法

当需要求一个点的函数图像的切线时,只需要求函数在该点的斜率,即导数,需要与其相邻的点一起计算。 显然,两点越近,结果越准确。 于是有如下公式:

unity地形贴图_unity 生成法线贴图_法线凹凸贴图

求切线后,得到两个方向的切线向量

. 之所以是这种形式的二维向量,是因为它是根据uoh平面和voh平面计算的,具体的向量形式需要根据实际情况进行组合。这里可以做一个优化。 在计算导数的时候,在公式中做了一个除法,因为法线最终会被归一化,而切向量的长度并不影响叉积后结果向量的方向,所以实际上,计算时的除法导数可以直接Remove,即直接将切向量乘以

, 变成

. 如果觉得乱3D动画,没关系,后面看具体代码就明白了。

下一步是对两个向量进行叉积。 叉积的顺序会影响计算法线的方向,应根据实际情况确定。

例子

本例使用Unity Shader动态生成纹理中每个像素的法线,并输出为颜色,最后在屏幕上看到动态生成的法线贴图。 将纹理定向为与屏幕平行,如下图所示:

unity 生成法线贴图_unity地形贴图_法线凹凸贴图

整个贴图在世界空间的XOY平面上,朝向-Z轴(Unity使用左手坐标系,Z轴朝向屏幕)。

由于没有高度图,所以提取灰度值作为高度图。 算法同上,函数名为GetGrayColor。

然后可以根据高度图的值计算uv两个方向的高度函数切线。

法线凹凸贴图_unity地形贴图_unity 生成法线贴图

上面的代码分为3段,前两段是计算高度函数在uv各个方向的切线,最后一段是计算最终的法线。

先看第一段,计算u方向的高度函数切线。首先,确定步长

的大小。

MainTexTexelSize是Unity Shader的一个内置变量,它存储了与纹理大小相关的信息,是一个float4类型的值,具体为(1/width,1/height,width,height)。

_DeltaScale是控制步长缩放的变量,本例中为0.5,乘以_DeltaScale用于控制法线生成的精度,如前所述,

值越小,生成的法线越准确。 通常我们会采样到当前采样点的两侧,以获得更准确的结果。 这种方法称为中心差分法。 然后就可以根据步长取当前像素左右两边的高度值(本例中为灰度值),按照上面说的计算方法计算切线。 注释掉的代码就是原来的代码,下面没有注释掉的代码就是优化后的代码,上面也有说到。

一个问题是,为什么计算出的切向量是(x, 0, z) 而不是其他形式? 这是因为上面提到了整个纹理在XOY平面上,高度是三维的。 因为u和v自然是按照x和y轴处理的,所以高度h是按照z轴处理的。

另一个可能的问题是,当_DeltaScale很小时,两边的像素其实都是单前像素,高度差为0。但实际上这种情况只有在采样滤波方式为点采样时才会出现。 如何处理,具体的采样过滤方法可以参考其他资料。

同理,第二段可以计算高度函数在v方向的切线,将两个切向量叉积,然后归一化得到当前像素点表面的法向量。 叉积的顺序很重要,因为贴图是朝向-z轴的,所以一般来说法线也会跟着表面的方向,这也是为什么是cross(tangentv, tangentu)而不是cross (tangentu, tangentv) 的原因。

现在将法线输出为颜色。 当然不能直接输出,因为法向量可能包含负值,你看到的可能是黑色的,所以需要进行转换。 此转换适用于了解法线贴图的读者。 应该很熟悉了。

fixed4color = 正常 * 0.5 + 0.5

直接输出这个颜色,如下图:

unity 生成法线贴图_法线凹凸贴图_unity地形贴图

它看起来与法线法线贴图有点不同,常见的是偏蓝的。 为什么是蓝色,因为常见的法线贴图都在切线空间。

基于切线空间的法线贴图,z或b通道的值为0.5到1,而x和y,即r和g通道都是0到1,所以看起来更蓝,当然不是绝对的。 对于上面计算的法线贴图,由于叉积的顺序,z分量是朝向-z轴的,所以b通道是0到0.5。 不信可以用截图工具看看色值。 在这种情况下unity 生成法线贴图,在切线空间中转换为法线贴图就像将 z 分量乘以 -1 一样简单。

normal.z *= -1;

fixed4color = 正常 * 0.5 + 0.5

结果如下:

unity地形贴图_unity 生成法线贴图_法线凹凸贴图

和上图相比,确实更蓝了,但还是不够蓝。 这倒不是因为这个贴图特别,而是还有一些修正步骤没有做。

计算切线向量时,高度差和

这个值是计算出来的,其实是不合理的,因为

非常非常小,一张1024*1024大小的图片,

只有1/1024 = 0.00097656,但是高度差是0和1之间的两个数的差值,比如0.6的高度和0.2的高度,一般比

是的,这导致切线向量非常接近-z轴,计算出的法线非常接近xoy平面,所以看起来红色和绿色很多,因为x和y组件更大。 为了解决这个问题,需要引入一个_HeightScale变量来控制高差的比例。

unity 生成法线贴图_法线凹凸贴图_unity地形贴图

当_HeightScale这个值的值为0.01时,法线贴图结果如下:

unity 生成法线贴图_unity地形贴图_法线凹凸贴图

法线贴图看起来很正常,仔细看会发现每块砖的顶部偏绿是因为y对应g,右侧偏红是因为x对应r。

我可以使用中心差分法吗?

可以使用有限差分法,即不取像素两侧的相邻点,而是只取一个方向的相邻点与当前像素进行比较,这种方法一般不如中心差异法。

除了高差缩放,还有其他参数可以调整吗?

是的,这里是两个简单的例子,因为修改的很简单,而且效果不适合这里说的例子,所以本文就不实现了。

碰撞值

图片中的每块砖是凹​​进去的还是凸出来的? 改变这个属性unity 生成法线贴图,只需要调整法线xy的正负,原来的凹凸方向就会改变。 你应该能够用一点想象力来解决它。

粗糙度

法线贴图的粗糙度可以在原法线贴图的基础上进一步修改。 其实之前的高差缩放也是处理粗糙度的,但是当你有一张已经生成的法线贴图,想要修改的话还需要做额外的处理。也很简单,缩放法线的xy分量,然后重新计算

就是这样。

加光

法线是给光照用的,所以这里我们来试试加平行光后的漫反射效果,和不加法线贴图的效果对比(默认法线是-z轴方向)。

第一种是没有法线贴图的情况。

unity地形贴图_unity 生成法线贴图_法线凹凸贴图

最终结果如下图所示:

法线凹凸贴图_unity 生成法线贴图_unity地形贴图

这是将光源绕x轴和y轴旋转60度,使用默认法线得到的漫反射结果。 与没有光照的原图相比,有明暗变化,但仍只是平面图。

接下来是使用上述算法动态生成法线贴图的情况。

unity 生成法线贴图_法线凹凸贴图_unity地形贴图

注意这里的normal.z不再乘以-1,因为这个例子中的一切都是在世界空间中计算的。 一般情况下,在切线空间计算可能效率更高,但这不是本文的内容。 最终输出结果如下图所示:

unity地形贴图_法线凹凸贴图_unity 生成法线贴图

可以看到整个画面的立体感很明显,砖块也比较粗糙,比之前的效果有了很大的提升。 仔细观察可以发现,每块砖的左边和上边都是亮的,右边和下边是暗的。 这符合平行光的旋转角度,所以照明结果是正确的。

最后的工作

最后的工作就是将生成的法线贴图保存到硬盘中。 这一步只需要调用引擎的相关API将渲染好的法线贴图保存为资源即可,也可以直接在CPU上操作生成一张,但是这样不方便使用实时光照查看效果。