概述:图形渲染本质即是CPU+GPU编程,这比传统的CPU编程更加复杂,就连其存储数据的位置,CPU和GPU对数据的访问权限都变的更加复杂。IA阶段是整个渲染过程中数据加载阶段,把这个模块学好对渲染整个渲染过程的理解都有很好的帮助。
模型文件里面的信息,会加载到内存中,然后映射到ID3D11Buffer里面,有了数据管道才能开始渲染。这里面分两个过程,一是创建buffer的过程,这一步会把数据存储在相应的位置。二是IASet,这个过程是把数据输送到渲染管道,提供使用。所以,实现的时候创建buffer会在程序初始化的时候,而IASet是在渲染的时候,你有可能创建了100个buffer而实际渲染的时候只渲染了10个。
整个渲染管道是由多个阶段组成的,而这所有的阶段都是由ID3D11DeviceContext进行控制的,它控制的第一个阶段就是IA阶段。IA是整个渲染过程数据准备阶段,包括格式的定义和数据的输入。这个阶段会向管道输入四种类型的对象:
IAGetIndexBuffer :获得一个指向索引缓冲区,也势必将输入汇编阶段。
IAGetInputLayout :获取一个指针,指向输入布局对象被绑定到输入汇编阶段。
IAGetPrimitiveTopology :获取信息的基本类型和数据顺序来描述的输入数据输入汇编阶段。
IAGetVertexBuffers :获取顶点缓冲区的输入汇编阶段的必然。
IASetIndexBuffer :索引缓冲区绑定到汇编器的输入阶段。
IASetInputLayout :输入布局对象绑定到汇编器的输入阶段。
IASetPrimitiveTopology :绑定信息的基本类型和数据顺序来描述的输入数据输入汇编阶段。
IASetVertexBuffers :顶点缓冲区数组绑定到汇编器的输入阶段。
这个过程其实是向管道传入四个对象:2个ID3D11Buffer对象,一个ID3D11InputLayout对象 ,一个
VertexBuffer 和 IndexBuffer:(分别描述顶点和顶点的索引)
1.定义顶点格式:用一个结构体描述顶点的信息,渲染的时候用到
struct VertexInput // 结构体的名字任意取,成员的类型也不是必须得用D3DX的类型,关键得hlsl里面有这样的类型即可。 { Vector3 position; // 位置 Vector3 normal; // 法向量 Vector3 tangent; // 切线 Vector3 bitangent; // 双向切线 Vector2 texture; // 纹理坐标 Color4 color; // 顶点颜色 };
这个是我自己定义的格式,根据不同需要灵活定义。
2.CreateBuffer :
HRESULT CreateBuffer( [in] const D3D11_BUFFER_DESC *pDesc, [in, optional] const D3D11_SUBRESOURCE_DATA *pInitialData, [out, optional] ID3D11Buffer **ppBuffer );
最终会得到一个ID3D11Buffer对象,但是要想创建必须传入上述两个结构体对象。
typedef struct D3D11_BUFFER_DESC { // 这个结构体对buffer的结果进行描述,站多大内容空间之类的,这样好开辟内存空间。 UINT ByteWidth; // 缓冲区大小,单位为字节 D3D11_USAGE Usage; // 一个枚举,描述的是GPU和CPU对此buffer的读写能力,其实这涉及到这个buffer将会存储在那个地方 UINT BindFlags; // 这个描述的是buffer的类型 UINT CPUAccessFlags; // 设置CPU访问权限,不访问设置为0 UINT MiscFlags; // 如果不使用设置为0,可以多选,暂时不知用法 UINT StructureByteStride; // ??? 不理解 } D3D11_BUFFER_DESC; typedef enum D3D11_USAGE { // 楼上的第二个参数 D3D11_USAGE_DEFAULT = 0, // 默认值,GPU具备读写能力 D3D11_USAGE_IMMUTABLE = 1, // GPU可读,CPU不可访问,如果选择这种方式必须得初始化buffer,你懂的 D3D11_USAGE_DYNAMIC = 2, // GPU只读,CPU只写 D3D11_USAGE_STAGING = 3 // 这种是CPU可以完全控制,GPU只能复制数据,与0完全相反。 } D3D11_USAGE; typedef enum D3D11_BIND_FLAG { // 对应第3个参数,此标志是多选 D3D11_BIND_VERTEX_BUFFER = 0x1L, // 顶点缓存 D3D11_BIND_INDEX_BUFFER = 0x2L, // 索引缓存 D3D11_BIND_CONSTANT_BUFFER = 0x4L, // 常数缓存,不可与其他标志共用 D3D11_BIND_SHADER_RESOURCE = 0x8L, // DX11.1版本才可用 D3D11_BIND_STREAM_OUTPUT = 0x10L, // 选此标志,buffer可以用来输出,管道的输出阶段 D3D11_BIND_RENDER_TARGET = 0x20L, // ??? D3D11_BIND_DEPTH_STENCIL = 0x40L, // ??? D3D11_BIND_UNORDERED_ACCESS = 0x80L, // ??? D3D11_BIND_DECODER = 0x200L, // ??? D3D11_BIND_VIDEO_ENCODER = 0x400L // ??? } D3D11_BIND_FLAG; typedef enum D3D11_CPU_ACCESS_FLAG { // 对应第4个参数,多选,与第二个参数要结合使用. D3D11_CPU_ACCESS_WRITE = 0x10000L, D3D11_CPU_ACCESS_READ = 0x20000L } D3D11_CPU_ACCESS_FLAG;
buffer是渲染管道的数据基础,也是GPU和CPU的桥梁,而这个结构体它正是对buffer的各种信息的描述,以至于buffer能够以合理的大小放在合理的位置,并且具备合理的能力,从而提高性能,后续需要继续熟悉这些配置。
参考:D3D11_USAGE,D3D11_BIND_FLAG,D3D11_RESOURCE_MISC_FLAG
4.D3D11_SUBRESOURCE_DATA :
typedef struct D3D11_SUBRESOURCE_DATA { const void *pSysMem; // 指向数据源的指针 UINT SysMemPitch; // 步长,当缓存的是纹理的时候有用 UINT SysMemSlicePitch; // 同上 } D3D11_SUBRESOURCE_DATA;
这个结构比上面的简单,pSysMem此时指向数据存储在内存的源,最终它在创建buffer的时候被拷贝到一个新的位置。
5.IASetVertexBuffers:
void IASetVertexBuffers( [in] UINT StartSlot, // 开始的输入插槽,只需要set第一个buffer的时候指定,后面的会自动向后排,DX11最大有32个输入插槽,这个是并行输入数据的能力。 [in] UINT NumBuffers, // 这个是顶点缓冲区的数目,不能超过最大插槽数,这个方法传递的不是一个顶点buffer,而是一个顶点buffer的数组,这里指定的是数组的数目。 [in] ID3D11Buffer *const *ppVertexBuffers, // 顶点buffer的指针或者数组指针 [in] const UINT *pStrides, // 我理解这个是每个顶点信息的宽度 [in] const UINT *pOffsets // 偏移量 );
6.IASetIndexBuffer
void IASetIndexBuffer( [in] ID3D11Buffer *pIndexBuffer, // 所以缓存指针 [in] DXGI_FORMAT Format, // 索引节点的数据格式 [in] UINT Offset );
例子:mScene是assimp导入的模型
void SceneNode::initInstance(ID3D11Device* device) { for(int i=0;i<mScene->mNumMeshes;i++) { Mesh* mesh = mScene->mMeshes[i]; mVertexCount += mesh->mNumVertices; mIndexCount += mesh->mNumFaces * 3; } VertexData* vertices = new VertexData[mVertexCount]; unsigned long* indices = new unsigned long[mIndexCount]; int v_index = 0; int i_index = 0; for(int i=0;i<mScene->mNumMeshes;i++) { Mesh* mesh = mScene->mMeshes[i]; for(int j=0;j<mesh->mNumVertices;j++) { vertices[v_index].position = mesh->mVertices[j]; vertices[v_index].normal = mesh->mNormals[j]; vertices[v_index].tangent = mesh->mTangents[j]; vertices[v_index].bitangent = mesh->mBitangents[j]; vertices[v_index].texture = Vector2(mesh->mTextureCoords[0][j].x,mesh->mTextureCoords[0][j].y);//mesh->mTextureCoords[j]; vertices[v_index].color = Color4(mesh->mColors[0][j].r,mesh->mColors[0][j].g,mesh->mColors[0][j].b,mesh->mColors[0][j].a); v_index++; } for(int j=0;j<mesh->mNumFaces;j++) { const Face& face = mesh->mFaces[i]; indices[i_index] = face.mIndices[0]; i_index++; indices[i_index] = face.mIndices[1]; i_index++; indices[i_index] = face.mIndices[2]; i_index++; } } D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc; D3D11_SUBRESOURCE_DATA vertexData, indexData; // 设置顶点缓冲描述 vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT; vertexBufferDesc.ByteWidth = sizeof(VertexData) * mVertexCount; vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; vertexBufferDesc.CPUAccessFlags = 0; vertexBufferDesc.MiscFlags = 0; vertexBufferDesc.StructureByteStride = 0; // 指向保存顶点数据的临时缓冲. vertexData.pSysMem = vertices; vertexData.SysMemPitch = 0; vertexData.SysMemSlicePitch = 0; // 创建顶点缓冲. device->CreateBuffer(&vertexBufferDesc, &vertexData, &mVertex); // 设置索引缓冲描述. indexBufferDesc.Usage = D3D11_USAGE_DEFAULT; indexBufferDesc.ByteWidth = sizeof(unsigned long) * mIndexCount; indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER; indexBufferDesc.CPUAccessFlags = 0; indexBufferDesc.MiscFlags = 0; indexBufferDesc.StructureByteStride = 0; // 指向存临时索引缓冲. indexData.pSysMem = indices; indexData.SysMemPitch = 0; indexData.SysMemSlicePitch = 0; // 创建索引缓冲. device->CreateBuffer(&indexBufferDesc, &indexData, &mIndex); // 释放临时缓冲. delete [] vertices; vertices = 0; delete [] indices; indices = 0; }
buffer把数据传递到渲染管道,但是它只是描述了整个buffer有多大,buffer里面的数据的结构依旧不清晰,这样的话管道是无法处理这些数据的。inputlayout的主要作用就是描述出顶点buffer的细节,还有就是告知第一个处理数据的渲染器是谁。
HRESULT CreateInputLayout( [in] const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // 一个数组这个数组描述一个顶点,是一个数组描述一个顶点,不是数组里面的一个元素描述这个顶点。 [in] UINT NumElements, // 数组的长度 [in] const void *pShaderBytecodeWithInputSignature, // 一个已经编译的渲染器指针 [in] SIZE_T BytecodeLength, // 渲染器大小 [out] ID3D11InputLayout **ppInputLayout // 结果 );
刚才我们定义顶点buffer的时候,最先回定义一个结构体来描述顶点,这个结构体是CPU的描述是C++语言,到了GPU的时候HLSL不清楚。所以这里会有一个D3D11_INPUT_ELEMENT_DESC数组来描述顶点的结构,让GPU可以识别。每个D3D11_INPUT_ELEMENT_DESC对应顶点中的一个元素。
D3D11_INPUT_ELEMENT_DESC :
typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; // 这个是定义HLSL中的语义,HLSL就是通过这个语义来识别它。 UINT SemanticIndex; // 这个是语义取重名的时候,用来区分的序号,例如矩阵要描述里面的每一个元素,总不能取不同的名字吧,所以用序号区分 DXGI_FORMAT Format; // 数据格式,它对应HLSL的数据类型 UINT InputSlot; // 设置为0即可 UINT AlignedByteOffset; // 对齐的偏移量,不知道该如何计算,还好只需要设置为:D3D11_APPEND_ALIGNED_ELEMENT就好了 D3D11_INPUT_CLASSIFICATION InputSlotClass; // UINT InstanceDataStepRate; // 这个值必须为0 - -。 } D3D11_INPUT_ELEMENT_DESC; typedef enum D3D11_INPUT_CLASSIFICATION { 对应倒数第2个 D3D11_INPUT_PER_VERTEX_DATA = 0, // 输入的是每个顶点的数据 D3D11_INPUT_PER_INSTANCE_DATA = 1 // 输入的是每个实例的数据 } D3D11_INPUT_CLASSIFICATION;
在这个结构体里面,SemanticName和Format比较重要,如同C++与hlsl的接口暗号。
注意:这里有一个InputSlot和InputSlotClass官网上只是说了InputSlot是输入插槽并且最多15个,但是并没有说插槽是跟什么对应的,而InputSlotClass跟InputSlot是什么关系也没有说。个人理解InputSlot对应buffer,但是又怎么跟InputSlotClass对应呢?目前不知道这个地方怎么样的,只能在D3D11_INPUT_PER_INSTANCE_DATA的时候为instance创建一个VB然后作为第二个输入进去,然后把InputSlot设置为1。
AlignedByteOffset的计算方式:每一个32位的浮点站4个字节因此一个R32G32B32_FLOAT站12个,需要注意的是这里的计算是按照vs里面你定义的input的类型来进行计算的,而不是inputlayout里面你定义语义的格式。所以,计算的时候是float4为16个,起始为0。
例子:
D3D11_INPUT_ELEMENT_DESC polygonLayout[2]; unsigned int numElements; // 设置数据布局,以便在shader中使用. // 定义要和ModelClass中的顶点结构一致. polygonLayout[0].SemanticName = "POSITION";//vs中的输入参数 polygonLayout[0].SemanticIndex = 0; polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT; polygonLayout[0].InputSlot = 0; polygonLayout[0].AlignedByteOffset = 0; polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[0].InstanceDataStepRate = 0; polygonLayout[1].SemanticName = "COLOR"; polygonLayout[1].SemanticIndex = 0; polygonLayout[1].Format = DXGI_FORMAT_R32G32B32A32_FLOAT; polygonLayout[1].InputSlot = 0; polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT; polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA; polygonLayout[1].InstanceDataStepRate = 0; // 得到layout中的元素数量 numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]); // 创建顶点输入布局. ID3D10Blob* color_vsb = nullptr; renderManager->getVertexShaderBuffer("color.vs",&color_vsb); result = m_pd3dDevice->CreateInputLayout(polygonLayout, numElements, color_vsb->GetBufferPointer(), color_vsb->GetBufferSize(), &m_layout);
使用也很简单只需要调用: // 绑定顶点布局.m_immediateContext->IASetInputLayout(m_layout);即可。
原语的设定:
我的理解就是用来描述最终显示的是什么样子,我们操作的是顶点,但是最终显示的结果并不是满屏幕都是点,而显示成什么样子取决于原语的设定。
void IASetPrimitiveTopology( [in] D3D11_PRIMITIVE_TOPOLOGY Topology );
这里设置的参数是一个枚举类型D3D11_PRIMITIVE_TOPOLOGY,具体参考另外一篇文章。
其它:常量buffer的传递
渲染管道数据的获取很大一部分来源是从IA输入进去的,但是除此之外,有一些数据它只需要提供给某一个渲染器使用,例如世界转换矩阵,很明显只有顶点着色器才会用到。针对类似这种数据,DX11里面设定了一种常量buffer的类型,并且每个渲染器都有一个获取常量buffer的方法。
以VS渲染器为例:
void VSSetConstantBuffers( [in] UINT StartSlot, // 常量缓冲的位置 [in] UINT NumBuffers, // 常量缓冲的数量 [in] ID3D11Buffer *const *ppConstantBuffers // 常量缓冲指针,多个的画就是数组了 );
每一个渲染器能够获取的常量缓存是有限个的,这个个数为:StartSlot + NumBuffers < 一个常数。这个常数为:D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT。
从输入的类型为ID3D11Buffer可知,常量buffer跟顶点和索引的buffer是一样的,只不过类型不一样。