谈谈法线图的压缩

发表于2018-04-12
评论6 1.08w浏览
| 导语 贴图压缩是游戏开发中常见的一个问题,不过说到法线图的压缩,其实里面就有一些特殊的问题要处理。前段时间做了一次贴图通道的优化,打算用两个通道表示法线图并且和其他通道合并到一张图里,以减少采样次数。这个过程中陆续挖掘了一些问题,记录一下。

1.为什么要用3个通道来表示法线?

        我们通常会把法线贴图归一化成一个3元向量n(x,y,z)来表示,常识上来看,因为这个n是归一化的,所以用两个向量(x,y)已经可以表示这个3元向量了,可以减少数据存储,压缩我们的贴图量。这里的问题在于,对于某一个顶点的法线贴图值,这个说法是正确的,但是在pixel中的sample textrue做线性插值的时候,两元的法线就有问题了。对于一个插值点p,在3元向量插值下的法线值的z分量是对原来z的插值,而在2元向量下则变成了对xy插值后用1减掉取平方根,显然2元的插值和3元的插值是不等的,对(0,1,0)和(0,0,1)两个像素的中间插值后的法线归一化后大约是(0,0.7,0.7),而用2元向量插值即(0,1)和(0,0)则得到(0,0.5),推算出z方向后则变成了(0,0.5, 0.86),这是两个相差很大的向量方向,问题的根源在于我们不能编程gpu上对于贴图的采样插值方法。

           所以这里面用2通道代替3通道,其实对插值后的法线效果是有损失的,它会趋向将插值后的z值抬升,然而相当多的引擎仍然是这样做的,原因就在于幸运的是在现实中在切线空间的法线大多数的值z都趋向于1,且插值的两个方向的xy不会差很远,而当两个插值的z越趋近于1,2元插值和3元插值的结果就越相近。

如下我们可以比较一下一个3元(上)表示和2元(下)表示法线贴图的比较,看上去差别并不明显,只是2元的表示高光更加发散一点,因为这种插值更倾向于提高z的分量。

为了更能好的用2元插值模拟3元插值的情况,有一种叫做球极投影的方式(sterographic),我们上面普通的用1-x²+y²可以认为是向xy平面的正交投影,而球极投影在地理绘图中常用,它会导致越贴近xy平面的投影出来的xy分量也相应扩大,以抵消对z的增长。公式是这样的,存储的法线的xy两元表示为

pX = X / ( 1 + Z )
pY = Y / ( 1 + Z )

插值采样法线图后的法线值计算为

denom = 2 / ( 1 + pX * pX + pY * pY )
X = pX * denom
Y = pY * denom
Z = denom - 1

这种方式插值出来的结果更近似3元插值的结果。

我们可以看下球极投影(上图)和普通正交投影(下图)的2元法线存储的法线效果对比,高光的发散就相对好了一些。

到这里,我们清楚了用3个通道存储归一的法线本质上不是多余的,优化到2个通道是有损的,但是在现实中的法线图上大多数是可以接受的,并且也可以选择更加拟合的球极投影方式。

2.法线贴图和贴图压缩算法

除了2个通道对于法线的损失,其实更大的损失来自于贴图压缩。常见的压缩方式本来是对颜色图设计的,一旦应用到法线这种特殊性质的图上,可能会存在一些其他的问题。

 在gpu支持的各种贴图压缩算法中,最终目标都是一样的,用更少的bit数表示一个本来可能要用RGBA32bit来表示的像素。我们拿DXT格式距离,DXT1用4个bit来表示一个像素,DXT5则用8个bit。他们如何做到?

DXT1会对每4*4的block做一个统一压缩,每个block中的16个像素会先拟合到RGB空间的一条直线上,直线的两个端点各用一个RGB565的16bit的格式来表示,然后把端点之间均匀分成4段,每个像素拟合到离他最近的那个线段点上,这个线段点的index用2个bit的来表示,这样一个block公用了64个bit。但是DXT1表示不了alpha透明度的渐变,只能多用1个bit表示有或者没有透明。

所以有了DXT5,dxt5的做法类似,颜色即rgb部分完全同DXT1的压缩,额外的alpha部分则另外拟合到一条一维空间的线上,线的两个端点各用1个8bit的alpha值表示,而线段通常均分为4或者6段,每个pixel的alpha值存储一个3个bit的线段点index。这样可以看到一个block用64个bit表示rgb,用64个bit表示alpha。也能看出来在压缩后alpha的质量明显优于rgb的,因为1个alpha通道和3个rgb通道占用的bit数是一样的。

这里转到法线图的压缩:

1.最naive的方式,直接把法线的3个通道存在rgb或者2个通道存在rg,这是最差的方法,原因在于法线的向量有个特性,他们是归一化的,即所有的法线值在空间内不是均匀分布的,而是分布在一个球面上,而上述的压缩算法要把block内的所有值强行拟合到一个直线上,对于不规则的颜色值是可以达到不错的效果的,但是把一个规则的球面分布的向量值强行拟合到一个直线上,结果就是大部分的像素的值都是离真实值偏差极大的,因此法线图在压缩时比颜色图损失的大得多。下面是一个用rgb存储法线做压缩后的效果

我们看到了明显的锯齿,这就是球面分布的法线在三维空间强行拟合成直线的压缩算法导致的问题。

2.对于这个问题,一些文献写到尽量不去归一化你的法线,这样可以使你的法线的分布更加充满空间而不是局限于球面,这会提高压缩算法的质量,所以有一些非归一化的切线空间法线贴图其实一个目的是为了提高压缩质量。我们使用非归一化的法线压缩后的效果是这样的

其实改善并不多,主要也是跟本图制作的时候并没有做很大程度的非归一化。此外这种方式虽然能够改善,但是仍然并不稳定,我们期望待压缩的数据分布是完全随机没有相互关联的。

3 基于alpha通道压缩。 我们看到包括dxt5在内的很多压缩算法会把rgb和a分开压缩,因此可以利用这个把法线的x放在rgb的某一个通道,而把法线的y放在alpha通道,这样x和y是被分开处理的,他们的分布都是一个自然的0-1,并且相互之间没有关联,这样压缩后得到的法线贴图的损失是最少的。

4.到底哪一个分量放在alpha通道?我们看到在贴图的压缩算法中,alpha的信息量损失是最少的,它和rgb占用同等的位宽,意味着更重要的信息更应该放置在alpha通道,那么x还是y重要?从经验上看,x相比更重要一些,x是左右向的分量,y是前后向的,通常左右向的变化比纵深方向更敏感,因为纵深远处会更占用更少的屏幕像素,一般是把x放在a通道,而y放在rgb的某个通道?

5.rgb的其他两个通道可以存储其他贴图么? 从压缩算法来看,如果我们把rgb通道的两个通道清空,只用一个通道存储法线的y分量,那么在拟合直线的时候,就把一个三维空间向直线的拟合转变成了一维空间向直线的拟合,拟合的程度是最高的,block内的多数像素都会拟合到更离他真实值更相似的值。所以对于rgb三个通道完全清空两个通道存储y分量,用alpha存储x分量将得到最好的压缩效果,每个block的x和y都将得到满满的32个bit存储。清空两个通道(上)和不清空存储其他信息(下)得到的法线效果对比

3.其他问题

这种清空通道rgb两个通道,用一个通道存储x分量,用alpha存储y分量也正是dxtnm压缩格式的做法,而dx10以及一些gl扩展支持的BC5格式则更加直接,由两个通道构成,分别存储两个分量,单独压缩。大多数的压缩算法是基于颜色压缩的,颜色压缩是对rgb做整体压缩的,而法线的各个分量需要分开压缩,颜色压缩希望压缩数据分布随机,但是法线分布局限球面,所以法线的压缩才要特殊注意。

另外用3个通道存储法线真的浪费么,至少从DXT的压缩算法上看,即使用4个通道来存储法线都不浪费,2个通道损失pixel插值的准确性,3个通道将损失将法线的分量分开压缩的可能,而4个通道清空两个通道才是存储法线的最好方式。当然在追求性能的时候,使用4个通道的ga表示法线,而把rb不清空存储其他的贴图信息也是可以接受的,但是同理一定不要让rgb3个通道之间的信息存在关联,即空间分布的不均匀。例如如果用1张4个通道的图存储两套法线,那么无论如何都会导致其中一套损失惨重。

法线除了压缩的问题,其实这里还有值域分布的问题,我们通常把浮点数的法线存储于例如8 bit 0-255的数值范围,而法线分布于球面,这就导致了纯3个通道各自存储整数分量的方式存储法线,会导致存储的数据在球面的不均匀,即球面过渡映射到线性过渡导致的大量的法线存储了球面的对角位置,而我们真正大量集中的朝向z轴正方向附近的法线值获得的存储空间其实占比很小,也有一些相关算法解决这个问题。总之都是为了解决基于压缩贴图的位数限制最大程度的表现细节更丰富的法线。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引