现在很多延迟光照算法,都有保存normal的pass,为了节约资源,很多有把normal三个分量pack为两个分量的算法,以d3d的左手坐标为例,normal在camera空间。
  最开始有:
    pack:xy = norm.xy;
    unpack:norm.xy = xy;
               norm.z = -sqrt(1 - x^2 - y^2);
  这个方法开始用的很广泛,但它是错的,因为由于透视投影的关系,有些面并不朝向眼睛方向,但仍然能被渲染进buffer里,所以z的符号不能确定。
  stalker对它做了一个非常直接的改进:
      pack:xy = norm.xy * 0.5 + 0.5;
              x *= norm.z>0?1:-1;
      unpack:norm.xy = abs(xy) * 2.0 - 1.0;
                 norm.z = (x > 0?1:-1) * sqrt(1 - x^2 - y^2);
  简而言之就是把z的符号存到pack后的x里,因为pack后x一定是正数,从算法上来讲这是正确的,但在实际使用中,通常用来保存normal的texture每个分量不会超过16 bit,则在x和y接近1的时候,会有比较大的误差,这个时候z接近0,如果是一个大的平面,x的符号会在1和-1之间抖动,表现在图像上主要是灰白相间的条纹。
  cryengine3的present的也有一个pack的方法:
      pack:xy = normalize(norm.xy) * sqrt(norm.z * 0.5 + 0.5);
      unpack:norm.z = length(xy)^2 * 2 - 1;
                 norm.xy = normalize(xy) * sqrt(1 - norm.z * norm.z);
  意思是将z的值作为xy向量的长度。unpack的时候先由长度得到z,然后由于xy比例一致,解norm.xy/sqrt(1-norm.z*norm.z)=norm.xy/sqrt(norm.x*norm.x+norm.y*norm.y)=xy/sqrt(x*x+y*y)即可得到xy,光从算法上看这也是正确的,但实际上用起来也是有的问题的,一个是除0的问题,因为有normalize操作,在z接近1或-1的时候,unpack xy会得一个误差比较大的结果,光照效果会有跳跃,不过并不明显。另外一个是这个算法计算量比较多,导致本身的误差积累比较大,同一个平面,相机在不同角度会有不同亮度,这个有些情况下很明显。

  后面的两个方法,实际上在没有比较大的平面的情况下,问题有时候不太明显,而且在增加bit数之后问题会有改善,但既然增加了bit数,不如干脆存三个分量来的直接,而且不需要pack/unpack指令。到现在为止我仍然没有找到一个算法有非常满意的结果,仍然直接存三个分量是最满意的。

 

  嗯,刚发完,托小Q的福,看了http://developer.nvidia.com/object/real-time-normal-map-dxt-compression.html这个文章,压缩dxt5和3dc normalmap的方法,不过也同样适用在这里:

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

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

  目前看来这是个比较不错的办法,虽然稍微慢些,但没有边界值和数值精度的问题。比较稳定。