谈谈法线图的压缩

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

  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轴正方向附近的法线值获得的存储空间其实占比很小,也有一些相关算法解决这个问题。总之都是为了解决基于压缩贴图的位数限制最大程度的表现细节更丰富的法线。


<think>嗯,用户想分析两张法线之间的误差,需要知道如何进行差异分析或误差计算方法。首先,我得回忆一下法线的基本概念。法线通常存储的是表面法线方向的信息,每个像素的RGB值对应法向量的XYZ分量。所以,计算误差的话,应该比较对应像素的法向量差异。 可能的误差计算方法有哪些呢?首先想到的是欧氏距离,直接计算两个法线向量之间的欧氏距离。不过法线是单位向量,所以差异可能在方向而不是长度上。欧氏距离应该可行,但要注意归一化。 然后是角度差异,也就是计算两个向量之间的夹角,用点积公式转换成角度值。这可能更直观,因为角度直接反映方向差异。不过计算可能稍微复杂一点,需要用到反余弦函数。 还有颜色空间差异,因为法线通常以RGB形式存储,可能直接比较RGB通道的差异,比如逐像素的绝对值或平方差。不过这种方法可能不够准确,因为法线向量需要转换到正确的空间,比如切线空间,可能存在不同的编码方式,比如将法线分量从[-1,1]映射到[0,1],所以需要考虑解码是否正确。 另外,统计指标也很重要,比如平均误差、最大误差、均方根误差(RMSE),这些能给出整体的差异情况。还有直方或分布分析,看看误差的分布情况,是否存在局部区域误差较大。 用户可能需要具体的步骤,比如如何预处理法线,确保它们的坐标系一致,比如都是切线空间,并且归一化到单位长度。然后选择适当的误差度量方法,进行计算和可视化。 可能需要注意的问题:法线是否已经归一化,如果未归一化,可能需要先归一化再比较。此外,不同的法线可能使用不同的坐标系(比如DirectX和OpenGL的Y轴方向不同),需要统一坐标系后再比较,否则误差计算会有偏差。 用户的应用场景可能是在形学中,比如比较不同算法生成的法线,或者检测法线压缩后的误差。这时候需要选择最适合场景的误差方法,比如在视觉上,角度差异可能更有意义,而欧氏距离更适合数值分析。 可能还需要生成相关问题,比如如何可视化法线误差,或者如何处理不同坐标系的法线,或者如何优化法线的生成以减少误差。这些问题可以帮助用户进一步深入分析。 总结下来,步骤应该是:预处理(归一化、统一坐标系),选择误差计算方法(欧氏距离、角度差、颜色差异),应用统计指标,最后可视化和解释结果。</think>### 法线差异分析方法 #### 1. **法线预处理** - **归一化处理**:确保两张法线法线向量均为单位向量。法线通常存储为$[0,1]$范围的RGB值,需先解码为$[-1,1]$范围的向量并归一化: $$n_{\text{normalized}} = \frac{2 \times \text{RGB} - 1}{\|\text{RGB}\|}$$ - **坐标系对齐**:检查法线是否使用相同坐标系(如切线空间或世界空间),若不一致需进行坐标变换[^1]。 #### 2. **误差计算核心方法** - **欧氏距离法**:逐像素计算两个法线向量的欧氏距离: $$d(n_1, n_2) = \sqrt{(n_{1x} - n_{2x})^2 + (n_{1y} - n_{2y})^2 + (n_{1z} - n_{2z})^2}$$ - **角度差异法**:通过点积计算夹角(弧度或角度): $$\theta = \arccos(n_1 \cdot n_2)$$ - **颜色空间差异**:直接比较RGB通道的差异(需先归一化): $$\Delta_{\text{RGB}} = \frac{1}{3}\sum_{c\in\{R,G,B\}}|c_1 - c_2|$$ #### 3. **统计分析与可视化** - **全局指标**:计算平均误差、最大误差、均方根误差(RMSE): $$\text{RMSE} = \sqrt{\frac{1}{N}\sum_{i=1}^N d(n_{1i}, n_{2i})^2}$$ - **局部对比**:生成误差热力,标记高误差区域。 - **分布分析**:绘制误差直方,分析误差分布是否均匀。 #### 4. **代码示例(Python)** ```python import numpy as np import matplotlib.pyplot as plt def compute_normal_error(normal1, normal2): # 解码法线(假设输入为[0,1]范围的RGB像) n1 = 2 * normal1.astype(np.float32) - 1 n2 = 2 * normal2.astype(np.float32) - 1 # 归一化 n1_normalized = n1 / np.linalg.norm(n1, axis=2, keepdims=True) n2_normalized = n2 / np.linalg.norm(n2, axis=2, keepdims=True) # 计算欧氏距离 euclidean_dist = np.sqrt(np.sum((n1_normalized - n2_normalized)**2, axis=2)) return euclidean_dist # 可视化误差热力 error_map = compute_normal_error(normal_img1, normal_img2) plt.imshow(error_map, cmap='hot') plt.colorbar() plt.show() ``` #### 5. **注意事项** - **量化误差**:8位法线会引入约$1/255$的步长误差,高精度场景建议使用16位格式[^2]。 - **坐标系一致性**:OpenGL与DirectX的切线空间法线Y轴方向相反,需统一坐标系。 - **应用场景适配**:渲染误差敏感区域(如高光反射)需优先优化角度差异。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值