前言
由于在Direct3D 11中取消了固定管线,要想绘制图形必须要了解可编程渲染管线的流程,一个能绘制出图形的渲染管线最少需要有这两个可编程着色器:顶点着色器和像素着色器。
在阅读本章内容之前,你还需要先浏览下面的章节:
章节 |
---|
HLSL编译着色器的三种方法 |
学习目标:
- 初步了解渲染管线
- 能够动手编写第一个顶点着色器和像素着色器
- 掌握HLSL编译着色器的方法
- 掌握将着色器绑定到渲染管线上的代码实现
- 了解顶点缓冲区,掌握创建和绑定到输入装配阶段的方法
- 渲染出第一个三角形
DirectX11 With Windows SDK完整目录
Github项目源码
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
渲染管线概述(渲染流水线)
若给出某个3D场景的几何描述,并在其中架设一台具有确定位置和朝向的虚拟摄像机,那么渲染管线(rendering pipeline)是以此摄像机为观察视角而生成2D图像的一系列完整步骤。下图展示了DirectX11的渲染管线完整流程:
其中椭圆区域的着色器阶段是可编程的,而其余阶段只提供了有限的设置。默认情况下当前阶段的着色器为null时,可以跳过当前步骤。对于初学者来说最常用的是下面这种情况:
输入装配阶段
输入装配阶段会从显存中读取顶点和索引(如果有)数据,再将它们装配成几何图元。通常用到的图元拓扑有点、线段和三角形。
顶点着色器阶段
待图元被装配完毕后,其顶点就会被送入到顶点着色器阶段。我们可以把顶点着色器看作一种输入与输出皆为单个顶点的函数,只是输入的顶点和输出的顶点不仅内容上可能有变化,顶点包含的属性也可能有变化。
我们可以利用顶点着色器来实现许多特效,例如变换、光照和置换贴图。在顶点着色器中,不但可以访问输入的顶点数据,也能够访问纹理和其他存于显存中的数据(如变换矩阵与场景的光照信息)。
光栅化阶段
光栅化阶段需要完成的任务很多,其中包括对输出的顶点进行视口变换,这样x、y坐标都将以像素为单位表示。然后根据原顶点和图元,确定当前图元会出现在待绘制纹理的哪些像素位置上,根据图元的顶点信息,并利用透视校正插值法来计算出像素片段(pixel fragment)的位置、颜色(如果有)等信息。
对于三角形来说,不同的顶点顺序有不同的含义。默认情况下,三角形的顶点绕顺时针排布时(从摄像机朝着该三角形看)则说明当前三角形的面朝向当前摄像机,而三角形的顶点绕逆时针排布则说明当前三角形的面背对当前摄像机。对于背朝摄像机的三角形会被剔除,从而不进行光栅化。这种做法叫背面消隐。
在光栅化的时候,每个像素都是以它的中点位置进行插值计算的。如下图所示,若只有2x2的像素,黑色顶点为构成三角形的顶点,蓝色顶点则为光栅化时选择的顶点以及插值计算得到的结果。
像素着色器阶段
作为可编程着色器阶段,它会针对每一个像素片段进行处理(每处理一个像素就要执行一次像素着色器)。它既可以直接返回一种单一的恒定颜色,也可以实现如逐像素光照、反射及阴影等更为复杂的效果
输出合并阶段
通过像素着色器生成的像素片段会被移送至渲染管线的输出合并阶段。在这个阶段中,一些像素片段需要经过模板测试和深度测试来确定是否能留下,然后必要的时候还需要与绑定的缓冲区对应像素片段进行混合操作,可以实现透明等效果。
只有当我们接触到后续的管线阶段时,才会进行更加详细的展开叙述。
第一份HLSL代码
现在我们在项目中创建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 | 说明输出的颜色值将会直接保存到渲染目标视图的后备缓冲区对应位置 |
输入布局(Input Layout)
ID3D11Device::CreateInputLayout方法--创建输入布局
在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
则是按每个实例数据输入。
通过语义、数据类型和起始偏移量,我们就可以建立起C++顶点缓冲区数据和HLSL顶点之间的联系。
接下来就可以使用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]获取的输入布局
ID3D11DeviceContext::IASetInputLayout方法--输入装配阶段设置输入布局
下面的方法可以让我们使用刚创建好的输入布局:
void ID3D11DeviceContext::IASetInputLayout(
ID3D11InputLayout *pInputLayout); // [In]输入布局
顶点/像素着色器的创建
ID3D11Device::CraeteXXXXShader方法--创建着色器
从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方法--着色器或特效相关的初始化
下面展示了GameApp::InitEffect
方法的实现,其中输入布局的创建也需要放到这里。
CreateShaderFromFile
函数请到文章开头的 HLSL编译着色器的三种方法 查看。
bool GameApp::InitEffect()
{
ComPtr 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;
}
GameApp::InitResource方法
顶点缓冲区(Vertex Buffer)
顶点缓冲区的作用是,将顶点数组以缓冲区ID3D11Buffer
的形式提供给输入装配阶段。
ID3D11Device::CreateBuffer方法--创建一个缓冲区
要创建顶点缓冲区,首先需要填充好缓冲区描述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 m_pVertexBuffer = nullptr;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
ID3D11DeviceContext::IASetVertexBuffers方法--渲染管线输入装配阶段设置顶点缓冲区
创建好顶点缓冲区后,就可以在渲染管线输入装配阶段设置该顶点缓冲区了:
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个顶点为一组,只绘制第1、3、5个顶点构成的三角形(或索引数组每6个索引为一组,只绘制索引模6余数为0, 2, 4的三角形) |
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ | 抛弃所有索引模2为奇数的顶点或索引,剩余的进行Triangle 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
ID3D11DeviceContext::IASetPrimitiveTopology方法--渲染管线输入装配阶段设置图元类型
void ID3D11DeviceContext::IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY Topology); // [In]图元类型
该操作只需要设置一次即可。
ID3D11DeviceContext::*SSetShader方法--给渲染管线某一着色阶段设置对应的着色器
这里的*
可以是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;
}
GameApp::DrawScene方法
ID3D11DeviceContext::Draw方法--根据已经绑定的顶点缓冲区进行绘制
该方法不需要提供索引缓冲区:
void ID3D11DeviceContext::Draw(
UINT VertexCount, // [In]需要绘制的顶点数目
UINT StartVertexLocation); // [In]起始顶点索引
调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
通过指定VertexCount
和StartVertexLocation
的值我们可以按顺序绘制从索引StartVertexLocation
到StartVertexLocation + VertexCount - 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));
}
练习题
粗体字为自定义题目
- 尝试交换三角形第一个和第三个顶点的数据,屏幕将显示什么?为什么?
- 尝试用6个顶点绘制矩形表面
- 尝试用4个顶点绘制矩形表面(提示:
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
)
DirectX11 With Windows SDK完整目录
Github项目源码
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。