因为在Direct3D 11中取消了固定管线,要想绘制图形必需要了解可编程渲染管线的流程,一个能绘制出图形的渲染管线最少须要有这两个可编程着色器:顶点着色器和像素着色器。html
本章假定读者已经了解了渲染管线的工做原理,将直接来到编程实战。git
接下来的目标以下:github
这里将直接从一个已经编写好的HLSL代码入手。编程
在此以前你还须要知道如何编译着色器:数组
章节 |
---|
HLSL编译着色器的三种方法 |
DirectX11 With Windows SDK完整目录数据结构
Github项目源码app
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。ide
如今咱们在项目中建立HLSL文件夹,将全部的着色器代码放到这里。函数
在里面建立一个Triangle.hlsli
的文件,内容以下:布局
// Triangle.hlsli struct VertexIn { float3 pos : POSITION; float4 color : COLOR; }; struct VertexOut { float4 posH : SV_POSITION; float4 color : COLOR; };
接下来建立Triangle_VS.hlsl
文件,用于存放顶点着色器代码:
#include "Triangle.hlsli" // 顶点着色器 VertexOut VS(VertexIn vIn) { VertexOut vOut; vOut.posH = float4(vIn.pos, 1.0f); vOut.color = vIn.color; // 这里alpha通道的值默认为1.0 return vOut; }
最后建立Triangle_PS.hlsl
文件,用于存放像素着色器代码:
#include "Triangle.hlsli" // 像素着色器 float4 PS(VertexOut pIn) : SV_Target { return pIn.color; }
HLSL代码的语法和C/C++的语法很是类似,也许后面会开坑描述一下HLSL语言,不过如今先把注意力放在这份代码中比较特别的地方。
float3
和float4
都是内置的变量类型,能够看做是C++的struct
类型,支持多种构造方式和成员访问。除此以外,还有float
和float2
两种类型。对于float4
,它的四个成员分别为x
,y
,z
和w
而后具体讲述一下变量名后面的语义:
语义名 | 具体含义 |
---|---|
POSITION | 描述该变量是一个坐标点 |
SV_POSITION | 说明该顶点的位置在从顶点着色器输出后,后续的着色器都不能改变它的值,做为光栅化时最终肯定的像素位置 |
COLOR | 描述该变量是一个颜色 |
SV_Target | 说明输出的颜色值将会直接保存到渲染目标视图的后备缓冲区对应位置 |
在HLSL中,用于输入的结构体为:
struct VertexIn { float3 pos : POSITION; float4 color : COLOR; };
该项目与之对应的C++结构体为:
struct VertexPosColor { DirectX::XMFLOAT3 pos; DirectX::XMFLOAT4 color; static const D3D11_INPUT_ELEMENT_DESC inputLayout[2]; };
注意:DX SDK中的
xnamath.h
在Windows SDK中已经被抛弃,取而代之的则是要包含头文件directxmath.h
,XNA相关的数学库基本上都移植到这里了,除此以外,他们都已经被放入到名称空间DirectX中。
为了可以创建C++结构体与HLSL结构体的对应关系,须要使用ID3D11InputLayout
输入布局来描述每个成员的用途、语义、大小等信息。
还要留意的是,其中inputLayout
并非结构体VertexPosColor
的内部成员,而是静态成员,不占用该结构体的空间。咱们使用D3D11_INPUT_ELEMENT_DESC
结构体来描述待传入结构体中每一个成员的具体信息,定义以下:
typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; // 语义名 UINT SemanticIndex; // 语义索引 DXGI_FORMAT Format; // 数据格式 UINT InputSlot; // 输入槽索引(0-15) UINT AlignedByteOffset; // 初始位置(字节偏移量) D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入类型 UINT InstanceDataStepRate; // 忽略 } D3D11_INPUT_ELEMENT_DESC;
inputLayout
的初始化信息以下,描述了C++对应到HLSL的两个成员的信息:
const D3D11_INPUT_ELEMENT_DESC VertexPosColor::inputLayout[2] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0} };
其中,语义名要与HLSL结构体中的语义名相同,如有多个相同的语义名,则语义索引就是另一种区分。相同的语义按从上到下因此分别为0,1,2...
而后,DXGI_FORMAT
在这里一般描述数据的存储方式、大小。用DXGI_FORMAT_R32G32B32_FLOAT
仅仅是解释为3个float类型的值;而用DXGI_FORMAT_R32G32B32A32_FLOAT
在这里是说明颜色按RGBA存储,而且为4个float类型的值
输入槽这里只使用1个,即索引为0的输入槽。
初始位置则指的是该成员的位置与起始成员所在的字节偏移量。
输入类型有两种:D3D11_INPUT_PER_VERTEX_DATA
为按每一个顶点数据输入,D3D11_INPUT_PER_INSTANCE_DATA
则是按每一个实例数据输入。
接下来就可使用ID3D11Device::CreateInputLayout
方法建立一个输入布局:
HRESULT ID3D11Device::CreateInputLayout( const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]输入布局描述 UINT NumElements, // [In]上述数组元素个数 const void *pShaderBytecodeWithInputSignature, // [In]顶点着色器字节码 SIZE_T BytecodeLength, // [In]顶点着色器字节码长度 ID3D11InputLayout **ppInputLayout); // [Out]获取的输入布局
下面的方法可让咱们使用刚建立好的输入布局:
void ID3D11DeviceContext::IASetInputLayout( ID3D11InputLayout *pInputLayout); // [In]输入布局
从D3D设备能够建立出6种着色器:
方法 | 着色器类型 | 描述 |
---|---|---|
ID3D11Device::CreateVertexShader | ID3D11VertexShader | 顶点着色器 |
ID3D11Device::CreateHullShader | ID3D11HullShader | 外壳着色器 |
ID3D11Device::CreateDomainShader | ID3D11DomainShader | 域着色器 |
ID3D11Device::CreateComputeShader | ID3D11ComputeShader | 计算着色器 |
ID3D11Device::CreateGeometryShader | ID3D11GeometryShader | 几何着色器 |
ID3D11Device::CreatePixelShader | ID3D11PixelShader | 像素着色器 |
这些方法的输入形参都是一致的,只是输出的是不一样的着色器,以建立顶点着色器的方法为例:
HRESULT ID3D11Device::CreateVertexShader( const void *pShaderBytecode, // [In]着色器字节码 SIZE_T BytecodeLength, // [In]字节码长度 ID3D11ClassLinkage *pClassLinkage, // [In_Opt]忽略 ID3D11VertexShader **ppVertexShader); // [Out]获取顶点着色器
下面展现了GameApp::InitEffect
方法的实现,其中输入布局的建立也须要放到这里。
CreateShaderFromFile
函数请到文章开头的 HLSL编译着色器的三种方法 查看。
// 这里使用了filesystem头文件,除此以外还须要添加 // using namespace std::experimental; bool GameApp::InitEffect() { ComPtr<ID3DBlob> blob; // 建立顶点着色器 HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf())); // 建立并绑定顶点布局 HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout.GetAddressOf())); // 建立像素着色器 HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf())); HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader.GetAddressOf())); return true; }
顶点缓冲区的做用是,将顶点数组以缓冲区ID3D11Buffer
的形式提供给输入装配阶段。
要建立顶点缓冲区,首先须要填充好缓冲区描述D3D11_BUFFER_DESC
:
typedef struct D3D11_BUFFER_DESC { UINT ByteWidth; // 数据字节数 D3D11_USAGE Usage; // CPU和GPU的读写权限相关 UINT BindFlags; // 缓冲区类型的标志 UINT CPUAccessFlags; // CPU读写权限的指定 UINT MiscFlags; // 忽略 UINT StructureByteStride; // 忽略 } D3D11_BUFFER_DESC;
在这里须要详细讲述一下D3D11_USAGE
枚举类型对应的读写关系:
CPU读 | CPU写 | GPU读 | GPU写 | |
---|---|---|---|---|
D3D11_USAGE_DEFAULT | √ | √ | ||
D3D11_USAGE_IMMUTABLE | √ | |||
D3D11_USAGE_DYNAMIC | √ | √ | ||
D3D11_USAGE_STAGING | √ | √ | √ | √ |
对于D3D11_USAGE_DEFAULT
类型的缓冲区,应当使用 ID3D11DeviceContext::UpdateSubresource
方法来更新缓冲区资源,它的原理是将内存中的某段数据传递到显存中,而后再将该显存中的数据复制到在显存中的缓冲区。这种更新方式咱们是没法直接访问缓冲区的内容的。在绘制完成/开始前调用能够比较快地更新显存中的数据。
而对于D3D11_USAGE_DYNAMIC
类型的缓冲区,则应当使用ID3D11DeviceContext::Map
和ID3D11DeviceContext::Unmap
方法,将显存中的数据映射到内存中,而后修改该片内存的数据,最后将修改好的数据映射回显存中。这种更新方式咱们是能够直接获取来自显存的数据的,但代价就是更新的效率会比上面的方式更低一些。
因为目前的教程所涉及到的顶点缓冲区在建立后一般是不会修改的,所以将其设为D3D11_USAGE_IMMUTABLE
。
这里将建立包含三个顶点数据的缓冲区:
// 设置三角形顶点 // 注意三个顶点的给出顺序应当按顺时针排布 VertexPosColor vertices[] = { { XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }, { XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) } }; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0;
有了缓冲区描述,还须要使用D3D11_SUBRESOURCE_DATA
结构体来指定要用来初始化的数据:
typedef struct D3D11_SUBRESOURCE_DATA { const void *pSysMem; // 用于初始化的数据 UINT SysMemPitch; // 忽略 UINT SysMemSlicePitch; // 忽略 } D3D11_SUBRESOURCE_DATA;
子资源数据结构体的填充也很简单:
// 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices;
最后经过ID3D11Device::CreateBuffer
来建立一个顶点缓冲区:
HRESULT ID3D11Device::CreateBuffer( const D3D11_BUFFER_DESC *pDesc, // [In]顶点缓冲区描述 const D3D11_SUBRESOURCE_DATA *pInitialData, // [In]子资源数据 ID3D11Buffer **ppBuffer); // [Out] 获取缓冲区
演示以下:
ComPtr<ID3D11Buffer> m_pVertexBuffer = nullptr; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
建立好顶点缓冲区后,就能够在渲染管线输入装配阶段设置该顶点缓冲区了:
void ID3D11DeviceContext::IASetVertexBuffers( UINT StartSlot, // [In]输入槽索引 UINT NumBuffers, // [In]缓冲区数目 ID3D11Buffer *const *ppVertexBuffers, // [In]指向缓冲区数组的指针 const UINT *pStrides, // [In]一个数组,规定了对全部缓冲区每次读取的字节数分别是多少 const UINT *pOffsets); // [In]一个数组,规定了对全部缓冲区的初始字节偏移量
// 输入装配阶段的顶点缓冲区设置 UINT stride = sizeof(VertexPosColor); // 跨越字节数 UINT offset = 0; // 起始偏移量 m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
只要绘制的内容不变,该部分的设置则只须要进行一次便可,由于渲染管线中各个部分的设置方法一经调用就会当即生效。然而若是须要绘制不一样的内容或者效果,则须要在绘制前给渲染管线绑定好各类所需的资源。
D3D_PRIMITIVE_TOPOLOGY
枚举定义了许多种图元类型,一般会根据顶点缓冲区的顶点索引(若是有索引缓冲区则是根据这些索引的值)和装配方式进行解释,其中:
图元类型 | 含义 |
---|---|
D3D11_PRIMITIVE_TOPOLOGY_POINTLIST | 按一系列点进行装配 |
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP | 按一系列线段进行装配,每相邻两个顶点(或索引数组相邻的两个索引对应的顶点)构成一条线段 |
D3D11_PRIMITIVE_TOPOLOGY_LINELIST | 按一系列线段进行装配,每两个顶点(或索引数组每两个索引对应的顶点)构成一条线段 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP | 按一系列三角形进行装配,每相邻三个顶点(或索引数组相邻的三个索引对应的顶点)构成一个三角形 |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST | 按一系列三角形进行装配,每三个顶点(或索引数组每三个索引对应的顶点)构成一个三角形 |
D3D11_PRIMITIVE_TOPOLOGY_LINELIST_ADJ | 每4个顶点为一组,只绘制第2个顶点与第3个顶点的连线(或索引数组每4个索引为一组,只绘制索引模4余数为2和3的连线) |
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ | 绘制除了最开始和结尾的全部线段(或者索引数组不绘制索引0和1的连线,以及n-2和n-1的连线) |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ | 每6个顶点为一组,只绘制第一、三、5个顶点构成的三角形(或索引数组每6个索引为一组,只绘制索引模6余数为0, 2, 4的三角形) |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ | 抛弃全部索引模2为奇数的顶点或索引,剩余的进行Triangle Strip的绘制 |
Point List
Line list(左) or line strip(右)
Triangle list(左) or triangle strip(右)
Line list with adjacency(左) or line strip with adjacency(右)
Triangle list with adjacency(v6, v8, v10也构成一个三角形)
Triangle strip with adjacency缺图
一般绝大多数状况下,咱们都会使用D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
void ID3D11DeviceContext::IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY Topology); // [In]图元类型
该操做只须要设置一次便可。
这里的*
能够是V, H, D, C, G, P,对应六个可编程渲染管线阶段,除了第一个参数提供的着色器类型不一样外,其他参数一致。
以ID3D11DeviceContext::VSSetShader
为例:
void ID3D11DeviceContext::VSSetShader( ID3D11VertexShader *pVertexShader, // [In]顶点着色器 ID3D11ClassInstance *const *ppClassInstances, // [In_Opt]忽略 UINT NumClassInstances); // [In]忽略
注意: 相似给渲染管线绑定资源的一切方法,在绑定以后就会一直生效,而不是说仅可以使用一次。因此,之后若是你须要用别的特效去绘制当前物体,就要从新绑定好渲染管线所须要的一切资源。
最后给出GameApp::InitResource
方法的实现
bool GameApp::InitResource() { // 设置三角形顶点 VertexPosColor vertices[] = { { XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }, { XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) } }; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf())); // ****************** // 给渲染管线各个阶段绑定好所需资源 // 输入装配阶段的顶点缓冲区设置 UINT stride = sizeof(VertexPosColor); // 跨越字节数 UINT offset = 0; // 起始偏移量 m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset); // 设置图元类型,设定输入布局 m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout.Get()); // 将着色器绑定到渲染管线 m_pd3dImmediateContext->VSSetShader(m_pVertexShader.Get(), nullptr, 0); m_pd3dImmediateContext->PSSetShader(m_pPixelShader.Get(), nullptr, 0); return true; }
该方法不须要提供索引缓冲区:
void ID3D11DeviceContext::Draw( UINT VertexCount, // [In]须要绘制的顶点数目 UINT StartVertexLocation); // [In]起始顶点索引
调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
经过指定VertexCount
和StartVertexLocation
的值咱们能够按顺序绘制从索引VertexCount
到VertexCount + StartVertexLocation - 1
的顶点
GameApp::DrawScene
方法的实现以下:
void GameApp::DrawScene() { assert(m_pd3dImmediateContext); assert(m_pSwapChain); static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; // RGBA = (0,0,0,255) m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), black); m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // 绘制三角形 m_pd3dImmediateContext->Draw(3, 0); HR(m_pSwapChain->Present(0, 0)); }
最终的效果以下:
粗体字为自定义题目
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
)DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。