最近刚好有个项目要用到这个技术,就学习了下DXSDK的Sample里面的Instancing10。
实例化的技术是利用多个顶点缓冲区有效的合并,减少Draw Call的次数,从而有效的提高程序的性能。实例化比较适合于相同物体的重复渲染,例如粒子系统,草被模拟等等。
本文实现了一个非常简单,甚至简陋的实例化的Demo。虽然Demo非常简单,但是实例化的每个必要的元素都已经被体现出来了。相对于Dx的Sample而言简单的多,不过同样可以把这个技术简单解释清楚。
正文:
实例化适合于相同物体的反复渲染,例如草,粒子系统等。传统的渲染方法中,相同物体由于一些细节不同,例如位置,颜色等信息,使得Draw Call被迫调用多次。对于很小的元素,例如草,可能视野里面会有上万根草,那么如果仅仅因为纹理,方向甚至位置不同,就必须调用上万次DrawCall,从而严重降低了程序的性能。利用实例化技术,虽然这些Mesh在一些简单的细节上有不同,但是大体上还是一致的,所以可以仅仅用一个Draw Call就可以把所有的Mesh渲染出来,这样节省了CPU与GPU之间的带宽,非常高效。
下面是这次Demo的截图:
这个Demo的画面非常简单,就是一个静态的粒子集合的渲染。但是所有的球体都是利用一个Draw Call渲染出来的,就是说每一帧只Draw一次。那么下面我简单介绍下这个技术的基本原理:
首先需要了解的一个概念是IA(input assembler)。在VertexShader之前,我们的输入是一个或者几个顶点缓冲区,而这些顶点缓冲区并不是VS的输入,事实上VS的输入是经过IA处理后的信息。IA实际上是不可以编程的,它就像一个状态机一样,只能设置简单的几个状态,但是由于不同的应用程序对于IA的要求基本都一致,所以完全没有可编程的必要。IA的目的就是根据顶点缓冲区,图元拓扑结构以及输入的数据格式(Layout)生成VS所需要的信息。我们在渲染任何物体之前,都需要为IA设置好这几个状态,分别是通过IASetInputLayout,IASetVertexBuffers,IASetPrimitiveTopology来搞定的。
在简单的了解了IA之后,我们来看一下Instancing的基本思想。其实,实例化就是通过把每个实例的不同的信息存储在缓冲(可能是顶点缓冲,常量Buffer等)里面,然后利用过个顶点缓冲区来设置,从而使生成的每个顶点都包含有自己Custom的数据定义。举个简单的粒子,我们看到这个程序中的每个球体的位置是不同的。按照传统的做法,伪代码如下:
for( i : 0 to sphereNum )
{
set world matrix
draw sphere i
}
上面的方法,也是最笨拙的方法。仅仅因为每次的Draw Call调用中world matrix不同,就必须调用多次。而其实每次的调用都是很相似的。其实我们在set world matrix的时候,我们更新的是某一个constant value。从宏观的角度上来说,就是通过constant value来设置球体的位置信息。但是既然我们可以描述每个顶点的法线,纹理坐标等信息,我们完全可以为每个顶点描述world matrix信息,其实就是在VS的输入里面多加一个float4而已(这里我只用了float4,因为球体不需要旋转。如果需要旋转的话,完全可以加四个float4组成一个matrix)。下面的问题是,我们怎么样利用IA生成我们想要的信息:
假设我们需要渲染100个球体,每个球体256个三角形。那么我们不可能在CPU端写入256*100个三角形的顶点数据,因为这样以来,从CPU到GPU端的传输代价会非常大,而这些代价完全可以避免(当然,这里面如果预处理的话,这些代价也可以无视,但是这种方法实际上相当于用CPU去做GPU擅长的事情,代码看着很不舒服)。那么实际上我们应该怎么做呢?其实也很简单,Dx10帮我们提供了非常友好的接口。
这个Demo里面需要两个顶点缓冲区,一个来描述球体的顶点信息,256个三角形而已(768个顶点)。另一个用来描述位置信息,100个顶点而已。两个加起来还不到1k个顶点,相对于上面的768000个顶点少了上千倍。这些数据是要走PCIE总线的!那么似乎上面的信息无法描述这么多个球体,但是通过IA处理之后,我们完全可以达到同样的效果。唯一需要设置的就是Input Layout。
//the vertex layout
D3D10_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION" , 0 , DXGI_FORMAT_R32G32B32_FLOAT , 0 , 0 , D3D10_INPUT_PER_VERTEX_DATA , 0 } ,
{ "mTransform" , 0 , DXGI_FORMAT_R32G32B32_FLOAT , 1 , 0 , D3D10_INPUT_PER_INSTANCE_DATA , 1 }
};
利用上面连个顶点缓冲,我们要设置如上的layout。第一个element描述的是球体的顶点信息。第二个element描述的是位置信息。我们注意到第五个参数,D3D10_INPUT_PER_VERTEX_DATA/D3D10_INPUT_PER_INSTANCE_DATA,这里面如果是前者,IA会把顶点缓冲中的每个顶点当做顶点处理。而如果是后者,IA实际上是把顶点缓冲区中的每个元素当做实例来处理的。那么最后IA可以为我们生成 numberOfVertex * numberOfInstance 个顶点数据,这也正是我们想要的数据。
有了这些数据,我们就可以进行顶点处理了。看看VS中有什么变化:
//the input struct of the vertex shader
struct VS_INPUT
{
//the instance id
uint uInstanceID : SV_InstanceID;
//the position of the particle
float3 vPosition : POSITION;
//the instanced position
float3 vTransform : mTransform;
};//the default vertex shader
VS_OUTPUT DefaultVertexShader( VS_INPUT input )
{
//the output of the vertex shader
VS_OUTPUT vs_out;//transform the vertex
vs_out.vPosProj = mul( float4( ( input.vPosition + input.vTransform ) , 1.0f ) , ViewProjMatrix );//pass the normal
vs_out.vNormal = input.vPosition.xyz;//copy the color
vs_out.vColor = ColorBuffer[input.uInstanceID % 8];//return the output struct
return vs_out;
}
很简单的一个VS。但是这里我们注意加粗体的一行,这里我们为每个顶点变换的矩阵是ViewProjectionMatrix。我们没有做WorldMatrix变换,原因很简单,是因为我们根本就没有WorldMatrix这样一个常量。我们每个顶点的World Matrix都已经存储到了顶点结构中的vTransform中了。实际上input.vPosition + input.vTransform就相当于做了World Matrix的变换了。
通过上面的步骤,就可以渲染出来不同位置的球体了。但是我们注意到,VS的Input里面并没有Color这一属性,为什么球体会有不同的颜色呢?其实球体的颜色信息是存储到了Constant Array中的,因为简单的几种颜色就可以满足人的视觉需求了,完全没有必要每个球体都生成不同的颜色。所以这里面我只生成了8中基本的颜色。每个顶点中是有uInstanceID的属性的,通过对于这个属性求模运算,我们可以为每个球体选择出相应的颜色。这里的uInstanceID是IA为我们生成的,而不是我们自己写入的数据。
有了这些处理,我们就可以实现一个简单的实例化的Demo了,下面是源代码:
http://filer.blogbus.com/4730079/resource_473007912613770374.rar