前几天同公司同事聊天谈及一个非常有趣、高效的技术,用以实现快速绘制相同模型的多个实例,比如在一个场景里有很多树,而这些树都是相同的模型,只是位置、方向、大小、颜色不同,我们就可以使用这种技术提高渲染效率。
在最新的D3D9 SDK有例子演示了这个技术(Direct3D\Instancing下,如果没有可能是因为版本不够新),与一般的渲染方法的区别在于,一般的方法需要为每个模型设定一次stream source,虽然这些模型的顶点、索引都是一致的;而Instancing仅需要设置一次stream source,在一次DrawIndexedPrimitive调用中完成全部实例的绘制。在Instancing例子中提供了4种实现方法,分别是:
实现方法
需要Shader Model3
需要额外的顶点缓冲
需要额外的常数寄存器
受CPU限制
1. 硬件实现
是
是
否
否
2. Shader实现
否
否
是
否
3. Constants实现
否
否
是
是
4. 多流实现
否
是
否
是
如果用户的显卡非常强劲,支持Shader Model3(这意味着需要6系列以上的N卡,A卡暂时还没有支持),使用方法1会非常高效,它的方法是这样:通过使用多流技术,将顶点数据和每个实例的不同属性(例如颜色、位置、大小)通过顶点缓冲一次送入显卡,在vertex shader中读取这些属性,并变换顶点位置、输出颜色就ok了,vertex shader看起来是这样:
void VS_HWInstancing( float4 vPos : POSITION, float3 vNormal : NORMAL, float2 vTex0 : TEXCOORD0,
float4 vColor : COLOR0, float4 vBoxInstance : COLOR1,
out float4 oPos : POSITION, out float4 oColor : COLOR0,
out float2 oTex0 : TEXCOORD0 )
{
//vColor,vBoxInstance保存了每个实例的属性,vColor是颜色,vBoxInstance.xyz是位置
//vBoxInstane.w是绕y的旋转角度
vBoxInstance.w *= 2 * 3.1415;
float4 vRotatedPos = vPos;
vRotatedPos.x = vPos.x * cos(vBoxInstance.w) + vPos.z * sin(vBoxInstance.w);
vRotatedPos.z = vPos.z * cos(vBoxInstance.w) - vPos.x * sin(vBoxInstance.w);
...... //输出顶点位置和颜色
}
这里需要注意的就是顶点数据可能和每个模型实例属性的数据长度不相同,在vertex shader处理的时候为了能正确匹配顶点和其相关的属性数据,需要为每个流指定不同的频率,例如:
V( pd3dDevice->SetStreamSource( 0, g_pVBBox, 0, sizeof(BOX_VERTEX)) );
V( pd3dDevice->SetStreamSourceFreq( 0, D3DSTREAMSOURCE_INDEXEDDATA | g_NumBoxes ) );
V( pd3dDevice->SetStreamSource( 1, g_pVBInstanceData, 0, sizeof( BOX_INSTANCEDATA_POS ) ) );
V( pd3dDevice->SetStreamSourceFreq( 1, D3DSTREAMSOURCE_INSTANCEDATA | 1ul ) );
这样便告诉vertex shader每1/g_NumBoxes个顶点对应一个实例属性数据,最后就是注意一下顶点的格式申明是这样:
D3DVERTEXELEMENT9 g_VertexElemHardware[] =
{
{ 0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0 },
{ 0, 3 * 4, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0 },
{ 0, 6 * 4, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0 },
{ 1, 0, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 0 },
{ 1, 4, D3DDECLTYPE_D3DCOLOR, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR, 1 },
D3DDECL_END()
};
方法2和方法3都是通过使用显卡的常数寄存器来存储实例属性数据,不同之处在于方法2一次传送尽量多的数据到显卡(通过SetVectorArray函数),但如果实例数据太多,常数寄存器也不够用,那么可能会多次调用DrawPrimitive函数,每次传送数据的不同部分,如果是Shader Model2的显卡,由于拥有比Shader Model1显卡更多的常数寄存器,所以效率会更高;方法3则是每个实例都调用DrawPrimitive函数,通过SetVector每次输入实例数据到shader,这样的方法已经退化到普通的方法来处理多实例渲染的地步。
下图是4种方法在我的显卡上绘制1000个盒子的帧率比较:
可以看出,方法1具有明显的优势,在支持ShaderModel2的情况下,方法2也比较理想,但在真实应用情况下不可能使用那么多的常数寄存器;方法3、4因为受CPU制约,不能最大发挥GPU性能,所以效率是最差的。