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

上周末实现了打算在KlayGE 4.0中使用的render to texture array功能。于是自然而然想到在ShadowCubeMap这个例子中使用,用来在1个pass内直接生成cubemap。结果,不比不知道,一比吓一跳。在不同GPU上FPS如下:

NV 9800GT NV 480GTX AMD 5870
6 pass Cubemap 158.63 312.82 241.10
Dual Paraboloid 5.77 375.32 211.91
1 pass Cubemap 66.08 288.77 228.44
1 pass Cubemap with instance 105.37 281.34 224.10
1 pass Cubemap with instance GS NA 287.80 211.01

9800GT所在机器的CPU比后两套系统差得多,没法横向比较,只能纵向比较。后两套系统只有GPU不同,可以横向和纵向比较。

5种做法

  • 6 pass Cubemap是个基准,实现方法就是最通俗的,在光源的位置向6个方向各渲一遍场景,得到cubemap的shadow map。
  • Dual Paraboloid需要渲染2个pass,正反面各一次,把经过tessellation的场景参数化到抛物面坐标系上。这种方法看似不错,但实际上省不了多少draw(因为在高层就做了视锥剪裁),而且因为需要tessellation,开销会增加很多。在9800GT上,因为没有硬件的tessellator,这里用的是简单的instanced tessellation,每个三角形不管大小都固定切分5次,所以性能超级低下。
  • 1 pass Cubemap的做法是,把每个顶点在VS里面分别乘上6个model view projection,得到了6个position全都传给GS。在GS里把每个三角形根据不同的position生成6份,然后通过SV_RenderTargetArrayIndex传到不同的rt上,一个pass完成render to texture array。原先也试过在VS里面只是简单地把输入的position传给GS,而在GS里面完成乘矩阵的事情,结果更慢。
  • 1 pass Cubemap with instance的做法是用instance来生成6个顶点。这样在VS里面只需要根据SV_InstanceID选择乘上哪一个model view projection,在GS里面也不用生成6份。
  • 1 pass Cubemap with instance GS用到了D3D11新增的instance GS功能,让GS自己instance多个,而不用IA来进行instance的操作。

数据分析

  1. Dual Paraboloid需要做很细的tessellation,而tessellation正是NV Fermi的强项,所以DP在NV 480GTX上比AMD 5870快得多,甚至快于不必做tessellation的方法。
  2. 1 pass Cubemap with instance比1 pass Cubemap快一些,说明用IA来拆顶点的效率比GS高得多(因为GS更通用)。
  3. 1 pass Cubemap with instance GS和 pass Cubemap with instance速度差不多。instance GS的出现就是为了加速这种状况,但它似乎没有做到。
  4. 1 pass Cubemap比6 pass Cubemap慢了不少。而且如果没有在GS里面做frustum culling,会慢相当多!

对于3和4,有两种可能。第一是cubemap只有6个面,太少了,不够GPU发挥;这种可能性存在,但概率很低。更有可能是,GS太垃圾了,没法很有效地执行预期的操作。

结论

这样一测试,基本上可以认为,在render to cubemap的情况下,只要经过了GS,哪怕很简单的GS,都会对性能大打折扣。所以除非是类似于stochastic rendering、motion blur,其他时候用GS往往得不偿失。好在,D3D11的GPU上GS已经有所优化。比起D3D10的GPU来说,新一代GPU的GS性能损失少了很多。但本来寄希望于instance GS,结果没有太多的好处,不知道以后的GPU和驱动会不会有所改进。

另外,如果实在要做render to texture array,别忘了在GS里面自己做frustum culling以及back face culling。这样能极大地减少输出三角形的个数,从而提升GS的性能。