上周末实现了打算在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的操作。
数据分析
- Dual Paraboloid需要做很细的tessellation,而tessellation正是NV Fermi的强项,所以DP在NV 480GTX上比AMD 5870快得多,甚至快于不必做tessellation的方法。
- 1 pass Cubemap with instance比1 pass Cubemap快一些,说明用IA来拆顶点的效率比GS高得多(因为GS更通用)。
- 1 pass Cubemap with instance GS和 pass Cubemap with instance速度差不多。instance GS的出现就是为了加速这种状况,但它似乎没有做到。
- 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的性能。