转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2275

Normal map已经在实时应用中被广泛使用了,但长期以来一直有个缺陷:无法很好地做mipmap。所以在一定的视角和距离下,会出现明显的aliasing现象。在非实时渲染中,因为sampling rate很高,对于一个pixel,积分发生在计算完shading之后,也就是

[latex]spec = \sum {\left (\frac {N_{i} \cdot H}{|N_{i}|} \right )^\alpha}[/latex]

而实时渲染的sampling rate极其有限,一般只有1,积分是在建立mipmap的时候对normal做的,shading用的是积分后的normal:

[latex]spec = \left (\frac {(\sum {N_{i}}) \cdot H}{|\sum {N_{i}}|} \right )^\alpha[/latex]

很显然这两个不等价,造成了aliasing。

LEAN Mapping

I3D 2010的LEAN Mapping的出现就是为了解决这个问题,作为一种更好的Normal Mapping表达。在KlayGE 4.3的开发计划中,原本我打算实现它。但在探索的过程中,LEAN Mapping(包括进一步发展的CLEAN Mapping)虽然原理和实现都很简单,效果也很好,但它的缺点也展露出来了,而且相当严重:

  • LEAN需要5个通道(CLEAN需要4个通道),而且其中有的通道要求16-bit高精度的。
  • 使用的时候需要tangent frame,forward框架还好,但对deferred框架来说是非常难搞的事情。
  • Blinn-Phong需要改成Beckmann。

所以可以这么说,一旦要用LEAN Mapping,就需要把shading的整个过程都改了。这是相当难接受的事情。

Mipmapping normal maps

于是,剩下的选择就是往前走一步或者往后退一步。查了一下,这个方向似乎没有后续的工作。而LEAN Mapping的Related Work里提到了Toksvig 2005的一篇Mipmapping normal maps,看似有希望。 它其实也稍微修改了BRDF方程,加了一个称为Toksvig factor的系数(用自己名字命名…):

[latex]f_{t} = \frac{|N_{a}|}{|N_{a}|+\alpha (1-|N_{a}|)}[/latex]

其中Na是mipmap的时候生成的平均normal的长度。

修改之后的specular变成了:

[latex]spec = \frac{1+f_{t} \alpha}{1 + \alpha}\left (\frac{N_{a}\cdot H}{| N_{a}|} \right )^{f_{t} \alpha}[/latex]

原文说了公式前面的系数[latex]\frac{1+f_{t} \alpha}{1 + \alpha}[/latex]为的是保能量。按照基于物理的BRDF提供的保能量系数,这个公式需要改成:

[latex]spec = \frac{f_{t} \alpha+2}{8}\left (\frac{N_{a}\cdot H}{| N_{a}|} \right )^{f_{t} \alpha}[/latex]

对比原公式,区别只是把shininess上乘一个ft。Na/|Na|只是个normalize,在建立mipmap的时候本来就会做。直观上来说,这就表示了如果一个区域内的normal什么方向都有,那么平均normal就会很短,所以需要降低shininess,显得比较粗糙。

更进一步

Mipmapping normal maps里,用当年流行的做法,对[latex]N_{a} \cdot H[/latex]和[latex]{N_{a}} \cdot {N_{a}}[/latex]建立一个2D查找表,完成specular的计算。当然在现在的硬件上,已经没必要这么做了,这些都可以在PS里实时完成。所以唯一剩下的项就是ft。从公式可以看出,ft取决于两个项,[latex]|N_{a}|[/latex]和[latex]\alpha[/latex]。 [latex]|N_{a}|[/latex]是可以预先计算好,[latex]\alpha[/latex]则是根据材质在实时的时候才能得到。所以我的改进可以总结成,在建立normal map的mipmap时,不但对生成的平均normal做normalize,而且把它的长度存到另一张texture,称为NaLength map。在渲染的时候,读取NaLength的内容,计算出ft,乘到shininess上,接着完成后面的shader。总的来说改动很小。

效果上看,在shininess大的时候可以明显看出是否使用了NaLength的区别。以下两图分别是shininess = 1024的时候打开和关闭NaLength的结果:

NaLength On

NaLength off

在中间区域,mip level很低,所以两者完全相同。靠近边缘的地方,mip level逐渐增加,所以区别越来越大,没用NaLength的时候会出现更多不该有的亮点,运动起来会更明显。

就是这样一个简单的办法,完全克服了LEAN Mapping的那三个大缺点,同时改动量很小,适合在实际中使用。在做完这些工作之后,才发现了Wild West开发人员的一个blog,他们也做了完全一样的事情。相见恨晚啊。