D3D的绘制函数总结

时间:2023-02-07 23:24:12

UP结尾的是User Pointer数据,不需要CreateVertexBuffer/CreateIndexBuffer函数创建内存;DrawSubSet是通过属性子集来绘制,

其实也是通过顶点缓存和索引缓存来绘制的,子集的个数就是纹理材质的个数,不使用网格优化也可以绘制使用网格优化会产生属性表有利于提高绘制效率。

关于绘制batch和性能优化

每个设置设备状态到发出绘制命令的转换都将产生一个Batch( [bætʃ]一批 )。动态顶点缓冲的推荐使用模式是一个可以合并Batch的模式,即不断地填充顶点数据,但不立刻绘制,在缓冲填满时才提交绘制一次,当然能合并的前提是各个batch都使用相同的设备状态,即纹理、材质、RenderStates、变换矩阵等

DrawPrimitive

渲染一个没有索引编制的序列,在数据输入流SetStreamSource中指定的类型的基础几何数据。
内部不会创建动态顶点缓存,用户输入的顶点个数是固定的,利于绘制静态的顶点缓存性能高;也可以声明使用方式为D3DUSAGE_DYNAMIC,可以用D3DLOCK_NOOVERWRITE,D3DLOCK_DISCARD动态更新顶点数据信息,例如粒子系统就可以使用。
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);HRESULT DrawPrimitive(  [in]  D3DPRIMITIVETYPE PrimitiveType, // 基础图元的类型D3DPRIMITIVETYPE  [in]  UINT StartVertex, // 渲染开始的顶点下标  [in]  UINT PrimitiveCount // 指定图元类型的个数(和图元类型相关),最大个数需要检测D3DCAPS0支持.);
DrawPrimitive函数需要设置SetStreamSource。
SetStreamSource
绑定一个顶点缓存到一个设备数据流。Device->SetStreamSource(0, Triangle, 0, sizeof(Vertex));
HRESULT SetStreamSource(  [in]  UINT StreamNumber,//指定一个数据流,从[0, streamNum-1].  [in]  IDirect3DVertexBuffer9 *pStreamData, // 绑定到设备数据流的顶点缓存指针  [in]  UINT OffsetInBytes,// 输入的顶点缓存的偏移,从0开始是输入整个缓存  [in]  UINT Stride// 组件的字节跨度,一般是顶点的字节数(特别是FVF vertex shader中),declaration的时候                  // 必须大于或者等于stream size. 如果顶点缓存被渲染多次的时跨度要用0表示,告诉runtime不要增长顶点缓存偏移。
);
Device->CreateVertexBuffer(
        3 * sizeof(Vertex), // size in bytes是申请的连续内存字节数据
        D3DUSAGE_WRITEONLY, // flags使用方式
        Vertex::FVF,        // vertex format顶点格式
        D3DPOOL_MANAGED,    // managed memory pool内存池类型
        &Triangle,          // return create vertex buffer 返回的顶点缓存
        0);                 // not used - set to 0

Dra
wPrimitiveUP

不需要设置
SetStreamSource,要求所有数据
要求所有的数据都是放在stream0里面,所以不能使用多流。
顶点格式正常的声明就可以了,SetVertexDeclaration或者SetFVF都可以,其它状态设置什么的也都一样。
该函数会在内部创建一个动态顶点缓存,多拷贝一次用户顶点缓存的数据。在需要用D3DLOCK_DISCARD,D3DLOCK_NOOVERWRITE锁定更新动态纹理,动态顶点缓存时候使用能够获得更好的效率。但数据量较大的动态顶点缓存,或者是静态顶点缓存时候,使用DrawPrimitiveUp会丧失掉优势。
HRES
ULT Dra
wPrimitiveUP(
 D3DPRIMITIVETYPE PrimitiveType,// 要绘制的图元类型
unsigned int PrimitiveCount,// 图元的数量
const void *pVertexStreamZeroData,// 顶点数据指针
unsigned int VertexStreamZeroStride);  // 每个顶点的大小

PrimitiveType:要绘画的图元的种类。就是上面介绍的那六种类型。 
PrimitiveCount:要绘画的图元的数量。假设有n个顶点信息,绘画的图元类型是点列的话,那么图元的数量就是n;如果绘画的图元类型是线列的
话,那么图元的数量就是n/2;如果是线带的话就是n-1;三角形列就是n/3;三角形带就是n-2;三角形扇出是n-2。
pVertexStreamZeroData:存储顶点信息的数组指针
VertexStreamZeroStride:顶点的大小

首先,DrawPrimitiveUP内部其实就是一个dynamic vertex
buffer(动态顶点缓冲),和我们自己实现一个动态顶点缓冲没区别。一般情况下,DrawPrimitiveUP和用动态顶点缓冲的效率也没多大区别。也就是说DrawPrimitiveUP其实很高效的,而且简单易用。Irrlicht引擎几乎所有绘制都用的DrawPrimitiveUP,也很快的。
那为什么不推荐用?
原因一:DX8发布时显存容量已经有了很大提高,静态顶点缓冲可以缓存在显存或AGP内存里,从而节省带宽占用。所以推荐能用静态顶点缓冲的一定要用静态的。静态的可以比DrawPrimitiveUP和动态顶点缓冲都快很多。
原因二:DrawPrimitiveUP相对动态顶点缓冲而言,需要将用户内存里的顶点数据复制到内部动态顶点缓冲,即多了一次复制,如果顶点数量较大,复制开销也会加大。但很多程序里的动态缓冲设计并不太好,为了抽象或方便使用,也会复制一次数据。所以便丧失了这条优势。
原因三:这个比较复杂,我们知道一帧内Batch(批)的数量直接影响CPU的占用率,1G处理器30FPS下每帧700Batch左右就会占用
100%CPU。
每个设置设备状态到发出绘制命令的转换都将产生一个Batch( [bætʃ]一批 )。动态顶点缓冲的推荐使用模式是一个可以合并Batch的模式,即不断地填充顶点数据,但不立刻绘制,在缓冲填满时才提交绘制一次,当然能合并的前提是各个batch都使用相同的设备状态,即纹理、材质、RenderStates、变换矩阵等
原因四:DrawPrimitiveUP只支持一个顶点流。这其实是个不算是原因的原因。当然是只用一个顶点流时才用它。
综上所述,这其实是个优化问题,用动态顶点缓冲有可能做更多的优化,但如果做得不好,会比DrawPrimitiveUP差。如果正确使用了,但没有进一步的优化或者引擎的用法不具备可优化的特性,那么也就和DrawPrimitiveUP效率相当。
但事实上用动态顶点缓冲做错了的也很多,最常见的就是没有正确使用Lock标志位,用锁定静态缓冲的方法锁定,根本得不到动态缓冲的效果。另外用C#和MDX的,如果用返回数组的Lock方法重载,也完全没有意义,因为在内部整个缓冲被复制到数组,Unlock时再复制回去。

DrawPrimitiveUP对于顶点数据很少变化的情况下,不推荐使用DrawPrimitiveUP,但是在顶点数据经常变化的时候,或者对程序的性能要求不高的场合下,DrawPrimitiveUP是个不错的选择。实际上OpenGL默认的工作模式和DrawPrimitiveUP是一样的,也没见性能差多少。

我的GUI系统用的是DrawPrimitiveUP,因为顶点数据是动态更新的。DrawPrimitiveUP的内部实现也是创建了一个动态的buffer,然后自动填充调用DrawPrimitive渲染,可以自己做是一样的,但是系统优化的更好。有人做过测试,在GUI这种动态更新顶点数据的场合下,使用DrawPrimitiveUP的速度比使用普通的顶点缓冲区要快很多


DrawIndexedPrimitive

Device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12);

HRESULT DrawIndexedPrimitive(

  [in]  D3DPRIMITIVETYPE Type, // 图元的类型,不能使用D3DPT_POINTLIST
[in]  INT BaseVertexIndex, // 顶点缓存的第一个顶点索引的偏移位置偏移是因为当多个顶点缓存合并成一块全局缓存中时每块都有一个基准的offset值,例如合并多个顶点缓存减少渲染batch数,提高性能,但是较少使用
[in]  UINT MinIndex, // 顶点缓存开始渲染的最小值
[in]  UINT NumVertices, // 顶点的个数,第一个是 BaseVertexIndex+MinIndex开始算需要渲染的个数,非顶点缓存的个数
[in]  UINT StartIndex, // 索引缓存中的起始索引,也就是指索引缓冲数组的下标起始位置
[in]  UINT PrimitiveCount // 图元的个数,2 * 6 = 12个三角形
);

参数2:INT BaseVertexIndex-起始顶点索引
   MSDN中的原话是这样:Offset from the start of the vertex buffer to the first vertex. “从顶点缓冲的起始位置到第一个顶点的偏移量”,第一个顶点不就是起始位置么?不一定是,DrawIndexedPrimitive这个函数接受从顶点缓
冲的任何一个位置开始读入顶点数据,你可以从index0的位置开始读取顶点数据,那这时BaseVertexIndex就是0,也可以从index2的
位置开始读取数据,那这时BaseVertexIndex就是2,如此类推。

参数3:UINT MinVertexIndex-最小顶点索引(这里MSDN应该是个笔误,写成MinIndex了,应该是MinVertexIndex)
   MSDN:Minimum vertex index for vertices used during this call. This is a zero based index relative to BaseVertexIndex. 本次函数调用中,顶点缓冲中最小的顶点索引。MSDN解释中我认为最关键的一句是“relative to
BaseVertexIndex”-“相对与起始顶点”,即MinVertexIndex实际上也是个相对偏移量,例如,起始顶点是
index2,MinVertexIndex是0,
那么实际是从index2读入数据,起始顶点是index1,MinVertexIndex是1,那么
实际也是从index2读入数据,即实际上的第一个顶点位置在BaseVertexIndex+MinVertexIndex处,最终偏移
量=BaseVertexIndex+MinVertexIndex,这里很容易产生误解,就是认为MinVertexIndex是指顶点索引的最小值,
即总是0,如果这样那MinVertexIndex这个参数就没有意义了。当初MS如果把这形参声明为
OffsetRelativeToBaseVertexIndex应该是更明确一点,不会让人产生误解。
   另一个问题就是,理论上这个偏移量如果是负值也可能是有意义的,例如BaseVertexIndex是1,而MinVertexIndex是-1就是指的顶点缓冲中的第一个顶点,这是有意义的,但实际中如果给MinVertexIndex赋负值我没有试验过。一般情况下MinVertexIndex都是
0。

    PrimitiveCount图元的个数,对于绘制的几何图形而言,n-2和n/3指的都是索引的数量而不是顶点的数量,也就是说n是索引缓冲中索引的数量。所以算图元个数时候需要注意。

Device->CreateIndexBuffer(

        36 * sizeof(unsigned short), // 索引缓存的字节数,个数是三角形的个数*3,索引缓存个数还是没有减少的。

        D3DUSAGE_WRITEONLY, // 使用类型为只读类型,获取指针然后写入数据,数据将会批量提交

        D3DFMT_INDEX16, // D3DFMT_INDEX16是16bit,D3DFMT_INDEX32是32bit

        D3DPOOL_MANAGED, // D3DPOOL类型,创建在SM中,需要时候拷贝到AGP内存/显存中

        &IB,

        0);//保留

填充索引缓存, DX中三角形顺时针(左手定则)为正面, 逆时针为背面会被消隐,填充索引缓存时候正向该三角形,左手坐标系顺时针绕向填充即可;OGL则相反。 索引缓存的正向三角形,那么三角形顶点顺序都是顺时针的(平视该三角形)。

Device->SetIndices(IB);关联到数据流。

例子:

Device->CreateVertexBuffer(

5 * sizeof(Vertex), // size in bytes

D3DUSAGE_WRITEONLY, // flags

Vertex::FVF,        // vertex format

D3DPOOL_MANAGED,    // managed memory pool

&Triangle,          // return create vertex buffer

0);                 // not used - set to 0

Device->CreateIndexBuffer(

5*sizeof(WORD),

D3DUSAGE_WRITEONLY,

D3DFMT_INDEX16,

D3DPOOL_MANAGED,

&IB,

0

);

Vertex* vertices;

Triangle->Lock(0, 0, (void**)&vertices, 0);


vertices[0] = Vertex(-1.0f, 0.0f, 2.0f);

vertices[1] = Vertex(0.0f, 1.0f, 2.0f);

vertices[2] = Vertex( 1.0f, 0.0f, 2.0f);

vertices[3] = Vertex( 1.0f, 1.0f, 2.0f);

vertices[4] = Vertex( 2.0f, 2.0f, 2.0f);


Triangle->Unlock();


WORD* indexBuffer;

IB->Lock(0, 0, (void**)&indexBuffer, 0);

indexBuffer[0] = 0;

indexBuffer[1] = 1;

indexBuffer[2] = 2;

indexBuffer[3] = 3;

indexBuffer[4] = 4;

IB->Unlock();

// 设置顶点缓存数据流和索引缓存

Device->SetStreamSource(0, Triangle, 0, sizeof(Vertex));
Device->SetIndices(IB);
// 可以绘制三角形出来
Device->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0,3,0, 1);
// 可以绘制直线出来
Device->DrawIndexedPrimitive(D3DPT_LINELIST, 0, 3, 2, 3, 1);
// 不可以绘制直线出来,应该是BaseVertexIndex只能是顶点缓存间的偏移,不能是缓存内部的偏移
//Device->DrawIndexedPrimitive(D3DPT_LINELIST, 3, 0, 2, 3, 1);

DrawIndexedPrimitiveUP这个函数

不需要设置SetStreamSource,和SetIndices;应该也是内部维护了一个动态顶点缓存,当数据量大时候,或者静态数据不经常更新时候不应该使用该函数,当数据量不是很大且经常更新顶点缓存数据时候,特别是改动顶点缓存大小时候用该绘制函数会得到比较好的效果。具体使用用到时候再仔细研究下,现在先mark一下。HRESULT DrawIndexedPrimitiveUP(
  D3DPRIMITIVETYPEPrimitiveType, // 图原的类型
  UINTMinVertexIndex,  // 指定0
  UINTNumVertices,  // 指定需要渲染的顶点的数量(如一个矩形可以由4个顶点组成,然后通过顶点索引来达到渲染2个三角形的效果,那么这里就应该填写4,而不是6)
  UINTPrimitiveCount, // 要渲染的图原的数量(如一个矩形,由两个三角形组成,就应该填写2)
  CONST void *pIndexData, // 索引数据指针
  D3DFORMATIndexDataFormat, // 索引数据格式,一般为D3DFMT_INDEX16或D3DFMT_INDEX32 
  CONST void*pVertexStreamZeroData, // 顶点数据指针
  UINTVertexStreamZeroStride // 顶点大小一般为sizeof(顶点结构),顶点结构不能添加自己的结构否则会导致问题
);

最近被DrawIndexedPrimitiveUP这个函数搞崩了好几次,甚至连显卡的Blue Screen Of Death也出来凑热闹了,所以需要总结一下它崩溃的可能原因,理清下头绪:
1.与OpenGL的glXXXPointer一样,如果指针内存错误,崩溃是肯定的,这种bug比较好找.
DirectX的原因更复杂:
2.DrawIndexedPrimitiveUP不直接崩溃,而是在Present的时候崩溃.可能SetIndices的索引缓冲指针是野指针,LPDIRECT3DVERTEXDECLARATION9是野指针.

解决方法:使用Debug Runtime排除所有异常调用,这对显卡驱动程序蓝屏崩溃或异常现象的调试十分有效,如:
Direct3D9: Decl Validator: X249: (Element Error) (Decl Element [5]) Declaration can't map to fixed function FVF because blendweight must use D3DDECLTYPE_FLOAT1/2/3/4.
Direct3D9: (ERROR) :DrawIndexedPrimitive failed.

3.又发现一种DrawIndexedPrimitiveUP错误,当错误地把索引个数作为第2个参数传入(实际上第2个参数应该是顶点个数), 导致程序在运行一段不定时间,随机地,可能地,花屏了.此时DX8报告一个驱动程序的错误:
Direct3D8: (ERROR) :Driver not handled in DrawPrimitives2
Direct3D8: (ERROR) :Driver returned error: DDERR_INVALIDPARAMS  
Direct3D8: (ERROR) :Driver failed command batch. Attempting to reset device state. The device may now be in an unstable state and the application may experience an access violation.

另:在试图安装现在的DirectX Debug Runtime时发现先前网上的资料或MSDN里都没谈到现在的安装方法:把Developer Runtime\x86里的dll复制到System32目录,然后再运行Utilities\Bin\x86\dxcpl.exe切换到Debug Rumtime模式.运行REF模式需要Debug Runtime环境!

Reference:
http://linghuye.googlepages.com/MyownlittleDirectXFAQ.mht


网格对象用属性表的绘制

HRESULT DrawSubset(
[in] DWORD AttribId
);
1)创建网格对象
需要先创建网格:
ID3DXMesh*         Mesh = 0;
// 创建空的网格
填充顶点和索引缓存数据
hr = D3DXCreateMeshFVF(
12,
24,
D3DXMESH_MANAGED,
Vertex::FVF,
Device,
&Mesh);
// 预定义的函数中创建三级网格
D3DXCreateTeapot(
Device, &Mesh, 0);
// 从文件中创建网格,.x格式中可以直接创建网格,其它格式中需要解析填充空网格得到网格
hr = D3DXLoadMeshFromX(  
"bigship1.x",
D3DXMESH_MANAGED,
Device,
&adjBuffer,
&mtrlBuffer,
0,
&numMtrls,
&Mesh);
2)对网格对象填充属性缓存,每个三角形对应一个属性缓存id,属性缓存的id下标刚好和材质纹理一致
// 填充材质或者纹理数据后
// 也可以先进行网格优化,生成属性表后,属性表是属性缓存中属性类型的个数,记录的每个属性类型相关的顶点缓存和索引缓存的一些渲染所需要的检索值信息。
3)根据网格属性个数,用DrawSubset绘制网格对象
// 优化后,也是直接根据网格属性集个数,直接绘制三角网格出来,设置一次大型纹理信息,有效的提高了性能
for(int i = 0; i < NumSubsets; i++)
{
Device->SetTexture( 0, Textures[i] );
Mesh->DrawSubset( i );
}

HLSL图形绘制

用SetVetexShader启用顶点着色器;用SetPixelShader来启用像素着色器;在效果框架中用SetTechnique激活绘制手法,用Begin,BeginPass启用手法其实相当于启用效果Shader,绘制使用用Effect的Pass来绘制该绘制也是用DrawPrimitive系列函数或者网格对象的DrawSubset时调用绘制。