在上一章,咱们知道了如何使用几何着色器来从新组装图元,好比从一个三角形分裂成三个三角形。可是为了实现更高阶的分形,咱们必需要从几何着色器拿到输出的顶点。这里咱们可使用可选的流输出阶段来拿到顶点集合。html
注意: 本章末尾有大量的GIF动图!git
在此以前须要额外了解的章节以下:github
章节回顾 |
---|
15 几何着色器初探 |
Visual Studio图形调试器详细使用教程(编程捕获部分) |
DirectX11 With Windows SDK完整目录编程
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。app
如今咱们知道GPU能够写入纹理(textures),例如深度/模板缓冲区以及后备缓冲区。固然,咱们也能够经过渲染管线的流输出阶段让GPU将几何着色器输出的顶点集合写入到指定的顶点缓冲区(vertex buffer)。除此以外,咱们还可以指定不进行光栅化以及后续的全部阶段,仅让顶点数据通过流输出阶段。ide
在几何着色器中,最多四个流输出对象能够被设置,即几何着色器的入口函数中只容许设置四个流输出对象的参数。当多个流输出对象存在时,它们必须都要为PointStream
类模板,但容许模板参数不一样。输出的顶点回流到顶点缓冲区后能够再次进行一遍新的渲染管线流程。函数
上一章也提到,几何着色器的单次调用不能产出超过1024个标量。所以分配给全部流输出对象的标量总和不能超过1024。好比如今我有2个流输出对象,它们的结构体相同,容纳512个标量,那最多仅容许输出2个这样的顶点来分配给这2个流输出对象。布局
void ID3D11DeviceContext::SOSetTargets( UINT NumBuffers, // [In]顶点缓冲区数目 ID3D11Buffer * const *ppSOTargets, // [In]顶点缓冲区数组 const UINT *pOffsets // [In]一个数组包含对每一个顶点缓冲区的字节偏移量 );
该方法多容许设置4个顶点缓冲区。ui
每一个要绑定到流输出阶段的缓冲区资源必需要在建立的时候额外设置D3D11_BIND_STREAM_OUTPUT
绑定标签。
若偏移值设为-1,则会引发流输出缓冲区被追加到最后一个缓冲区的后面
顶点缓冲区绑定到流输出阶段的输出槽0操做以下:
UINT offset = 0; m_pd3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
若是咱们须要恢复默认的状态,则能够这样调用:
ID3D11Buffer* nullBuffer = nullptr; UINT offset = 0; m_pd3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);
注意: 若是使用的是当前绑定到输入装配阶段的顶点缓冲区,则绑定会失效。由于顶点缓冲区不能够同时被绑定到输入装配阶段和流输出阶段。
由于后续咱们是将每一阶输出的顶点都保存下来,即使不须要交换顶点缓冲区,但也有可能出现同时绑定输入/输出的状况。一种合理的绑定顺序以下:
// 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer * nullBuffer = nullptr; m_pd3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset); // ... m_pd3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get()); // ... m_pd3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
当渲染管线完成一次流输出后,咱们就能够用下面的方法来获取绑定在流输出阶段上的顶点缓冲区(固然你自己持有该缓冲区的指针的话就不须要了)
void ID3D11DeviceContext::SOGetTargets( UINT NumBuffers, // [In]缓冲区数目 ID3D11Buffer **ppSOTargets // [Out]获取绑定流输出阶段的顶点缓冲区 );
输出的顶点缓冲区引用数会加1,最好是可以使用ComPtr
来承接顶点缓冲区,不然就要在结束的时候手工调用Release
方法,若忘记调用则会引起内存泄漏。
接下来咱们须要指定数据会流向哪一个输出槽,首先咱们须要填充结构体D3D11_SO_DECLARATION_ENTRY
,结构体声明以下:
typedef struct D3D11_SO_DECLARATION_ENTRY { UINT Stream; // 输出流索引,从0开始 LPCSTR SemanticName; // 语义名 UINT SemanticIndex; // 语义索引 BYTE StartComponent; // 从第几个份量(xyzw)开始,只能取0-3 BYTE ComponentCount; // 份量的输出数目,只能取1-4 BYTE OutputSlot; // 输出槽索引,只能取0-3 };
其中,语义名SemanticName
用于指定在几何着色器的流输出对象对应的结构体中该语义描述的成员,而后用语义索引SemanticIndex
指定存在同名语义下用索引值标记的惟一成员。
而后StartComponent
和ComponentCount
用于控制该向量须要输出哪些份量。若StartComponent
为1,ComponentCount
为2,则输出的份量为(y, z)
,而要输出所有份量,则指定StartCompnent
为0, ComponentCount
为4.
输出槽索引OutputSlot
用于指定选择绑定流输出的缓冲区数组中的某一元素。
因为这里一个结构体只能指定某个输出流中的某一贯量,因此一般咱们须要像顶点输入布局那样传递一个数组来取出组合成特定顶点。
好比说如今顶点着色器输入的顶点和流输出的顶点是一致的:
struct VertexPosColor { DirectX::XMFLOAT3 pos; DirectX::XMFLOAT4 color; static const D3D11_INPUT_ELEMENT_DESC inputLayout[2]; };
输入布局描述以下:
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中的结构体以下:
struct VertexPosColor { float3 PosL : POSITION; float4 Color : COLOR; };
流输出的入口描述以下:
const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = { { 0, "POSITION", 0, 0, 3, 0 }, { 0, "COLOR", 0, 0, 4, 0 } };
这里对应的是索引为0的流输出对象,输出给绑定在索引为0的输出槽的顶点缓冲区,先输出语义为POSITION的向量中的xyz份量,而后输出COLOR整个向量。这样一个输出的顶点就和原来的顶点一致了。
接下来给出ID3D11Device::CreateGeometryShaderWithStreamOutput
方法的原型:
HRESULT ID3D11Device::CreateGeometryShaderWithStreamOutput( const void *pShaderBytecode, // [In]编译好的着色器字节码 SIZE_T BytecodeLength, // [In]字节码长度 const D3D11_SO_DECLARATION_ENTRY *pSODeclaration, // [In]D3D11_SO_DECLARATION_ENTRY的数组 UINT NumEntries, // [In]入口总数 const UINT *pBufferStrides, // [In]一个数组包含了每一个绑定到流输出的缓冲区中顶点字节大小 UINT NumStrides, // [In]上面数组的元素数目 UINT RasterizedStream, // [In]按索引指定哪一个流输出对象用于传递到光栅化阶段 ID3D11ClassLinkage *pClassLinkage, // [In]忽略 ID3D11GeometryShader **ppGeometryShader // [Out]建立好的几何着色器 );
若是不须要有流输出对象提供数据给光栅化阶段,则RasterizedStream
应当指定为D3D11_SO_NO_RASTERIZED_STREAM
。即使某一流输出对象传递了数据给光栅化阶段,它仍能够提供数据给某一绑定的缓冲区。
下面是一个调用的例子:
const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = { { 0, "POSITION", 0, 0, 3, 0 }, { 0, "COLOR", 0, 0, 4, 0 } }; HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout), &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, m_pTriangleSOGS.GetAddressOf()));
当该着色器被绑定到渲染管线上,流输出阶段就会被激活,咱们可使用ID3D11DeviceContext::Draw
方法来进行绘制。而后当渲染管线开始执行的时候,任何传递给几何着色器中的流输出对象的数据,都会基于语义名和语义索引尝试匹配输出布局。一旦发现有匹配的语义,该数据就会流向对应的缓冲区来建立完整的输出顶点集。
因为如今的着色器多到使人发指,并且有没有很好的办法归类整合,故在下面用一张表列出全部绘制流程用到的着色器hlsl文件名称:
操做 | VS | GS | PS |
---|---|---|---|
经过流输出获得分裂的三角形 | TriangleSO_VS | TriangleSO_GS | X |
经过流输出获得分形雪花 | SnowSO_VS | SnowSO_GS | X |
经过流输出获得分形球体 | SphereSO_VS | SphereSO_GS | X |
绘制分形三角形 | Triangle_VS | X | Triangle_PS |
绘制分形雪花 | Snow_VS | X | Snow_PS |
绘制分形球体 | Sphere_VS | X | Sphere_PS |
绘制法向量 | Normal_VS | Normal_GS | Normal_PS |
首先给出Basic.hlsli
文件的内容,要注意里面的常量缓冲区和以前有所变化:
#include "LightHelper.hlsli" cbuffer CBChangesEveryFrame : register(b0) { matrix g_World; matrix g_WorldInvTranspose; } cbuffer CBChangesOnResize : register(b1) { matrix g_Proj; } cbuffer CBChangesRarely : register(b2) { DirectionalLight g_DirLight[5]; PointLight g_PointLight[5]; SpotLight g_SpotLight[5]; Material g_Material; matrix g_View; float3 g_SphereCenter; float g_SphereRadius; float3 g_EyePosW; float g_Pad; } struct VertexPosColor { float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexPosHColor { float4 PosH : SV_POSITION; float4 Color : COLOR; }; struct VertexPosHLColor { float4 PosH : SV_POSITION; float3 PosL : POSITION; float4 Color : COLOR; }; struct VertexPosNormalColor { float3 PosL : POSITION; float3 NormalL : NORMAL; float4 Color : COLOR; }; struct VertexPosHWNormalColor { float4 PosH : SV_POSITION; float3 PosW : POSITION; float3 NormalW : NORMAL; float4 Color : COLOR; };
经过流输出阶段,一个三角形就分裂出了三个三角形,顶点的数目翻了3倍。若规定1阶分形三角形的顶点数为3,则N阶分形三角形的顶点数为\(3^{N}\)。
首先是TriangleSO_VS.hlsl
,它负责将顶点直接传递给几何着色器。
// TriangleSO_VS.hlsl #include "Basic.hlsli" VertexPosColor VS(VertexPosColor vIn) { return vIn; }
而后和上一章同样,TriangleSO_GS.hlsl
中的几何着色器将一个三角形分裂成三个三角形,而且输出的顶点类型和输入的顶点是一致的。
// TriangleSO_GS.hlsl #include "Basic.hlsli" [maxvertexcount(9)] void GS(triangle VertexPosColor input[3], inout TriangleStream<VertexPosColor> output) { // // 将一个三角形分裂成三个三角形,即没有v3v4v5的三角形 // v1 // /\ // / \ // v3/____\v4 // /\xxxx/\ // / \xx/ \ // /____\/____\ // v0 v5 v2 VertexPosColor vertexes[6]; int i; [unroll] for (i = 0; i < 3; ++i) { vertexes[i] = input[i]; vertexes[i + 3].Color = (input[i].Color + input[(i + 1) % 3].Color) / 2.0f; vertexes[i + 3].PosL = (input[i].PosL + input[(i + 1) % 3].PosL) / 2.0f; } [unroll] for (i = 0; i < 3; ++i) { output.Append(vertexes[i]); output.Append(vertexes[3 + i]); output.Append(vertexes[(i + 2) % 3 + 3]); output.RestartStrip(); } }
接下来的Triangle_VS.hlsl
和Triangle_PS.hlsl
则是常规的三角形绘制:
// Triangle_VS.hlsl #include "Basic.hlsli" VertexPosHColor VS(VertexPosColor vIn) { matrix worldViewProj = mul(mul(g_World, g_View), g_Proj); VertexPosHColor vOut; vOut.Color = vIn.Color; vOut.PosH = mul(float4(vIn.PosL, 1.0f), worldViewProj); return vOut; }
// Triangle_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHColor pIn) : SV_Target { return pIn.Color; }
如今规定第一张图为一阶分形雪花,第二张为二阶分形雪花。观察两者之间的变化,能够发现前者的每一条直线变成了四条折线。其中每一个尖锐角的度数都在60度,而且每条边的长度都应该是一致的。
和以前同样,SnowSO_VS.hlsl
中的顶点着色器阶段只用于顶点直传:
// SnowSO_VS.hlsl #include "Basic.hlsli" VertexPosNormalColor VS(VertexPosNormalColor vIn) { return vIn; }
而后重点就在于SnowSO_GS.hlsl
的几何着色器了。这里先放出代码:
// SnowSO_GS.hlsl #include "Basic.hlsli" [maxvertexcount(5)] void GS(line VertexPosColor input[2], inout LineStream<VertexPosColor> output) { // 要求分形线段按顺时针排布 // z份量必须相等,由于顶点没有提供法向量没法判断垂直上方向 // v1 // /\ // ____________ => ____/ \____ // i0 i1 i0 v0 v2 i1 VertexPosColor v0, v1, v2; v0.Color = lerp(input[0].Color, input[1].Color, 0.25f); v1.Color = lerp(input[0].Color, input[1].Color, 0.5f); v2.Color = lerp(input[0].Color, input[1].Color, 0.75f); v0.PosL = lerp(input[0].PosL, input[1].PosL, 1.0f / 3.0f); v2.PosL = lerp(input[0].PosL, input[1].PosL, 2.0f / 3.0f); // xy平面求出它的垂直单位向量 // // | // ____|_____ float2 upDir = normalize(input[1].PosL - input[0].PosL).yx; float len = length(input[1].PosL.xy - input[0].PosL.xy); upDir.x = -upDir.x; v1.PosL = lerp(input[0].PosL, input[1].PosL, 0.5f); v1.PosL.xy += sqrt(3) / 6.0f * len * upDir; output.Append(input[0]); output.Append(v0); output.Append(v1); output.Append(v2); output.Append(input[1]); }
能够发现分形雪花每升一阶,须要绘制的顶点数就变成了上一阶的4倍。
这里要求了z份量必须相等,由于使用的着色器仍把一切的顶点仍当作3D顶点来对待出来(固然你也能够写成2D的着色器)。
而后开始具体分析从直线变折线的过程,能够看到由于顶点v1所在角的度数在60度,且v0, v1, v2构成等边三角形,故v0v2,
v0v1和v1v2的边长是一致的。并且4条折线要求边长相等,故这里的i0v0和v2i1应当各占线段i0i1的1/3.
其中lerp
函数是线性插值函数,数学公式以下:
\[ \mathbf{p} = \mathbf{p}_0 + t(\mathbf{p}_1 - \mathbf{p}_0) \]
其中t的取值范围在[0.0f, 1.0f],而且操做对象p0和p1能够是标量,也能够是矢量,对矢量来讲则是对每一个份量都进行线性插值。
当t = 0.5f时,描述的就是p0和p1的中值或中点。
该函数很容易描述两点之间某一相对位置。
因为咱们规定了连续线段必须按顺时针排布,咱们就能够利用向量i0i1逆时针旋转90度获得对应的突出方向向量,而后标准化,乘上相应的高度值便可获得顶点v1的位置。
最后就是用于绘制的着色器代码:
// Snow_VS.hlsl #include "Basic.hlsli" VertexPosHColor VS(VertexPosColor vIn) { matrix worldViewProj = mul(mul(g_World, g_View), g_Proj); VertexPosHColor vOut; vOut.Color = vIn.Color; vOut.PosH = mul(float4(vIn.PosL, 1.0f), worldViewProj); return vOut; }
// Snow_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHColor pIn) : SV_Target { return pIn.Color; }
如下是一阶和二阶的分形圆球:
仔细观察能够看到,原先的一个三角形分裂出了四个三角形,即每升一阶,须要绘制的顶点数就变成了上一阶的4倍。
SphereSO_VS.hlsl
代码和SphereSO_GS.hlsl
代码以下:
// SphereSO_VS.hlsl #include "Basic.hlsli" VertexPosNormalColor VS(VertexPosNormalColor vIn) { return vIn; }
// SphereSO_GS.hlsl #include "Basic.hlsli" [maxvertexcount(12)] void GS(triangle VertexPosNormalColor input[3], inout TriangleStream<VertexPosNormalColor> output) { // // 将一个三角形分裂成四个三角形,但同时顶点v3, v4, v5也须要在球面上 // v1 // /\ // / \ // v3/____\v4 // /\xxxx/\ // / \xx/ \ // /____\/____\ // v0 v5 v2 VertexPosNormalColor vertexes[6]; matrix viewProj = mul(g_View, g_Proj); [unroll] for (int i = 0; i < 3; ++i) { vertexes[i] = input[i]; vertexes[i + 3].Color = lerp(input[i].Color, input[(i + 1) % 3].Color, 0.5f); vertexes[i + 3].NormalL = normalize(input[i].NormalL + input[(i + 1) % 3].NormalL); vertexes[i + 3].PosL = g_SphereCenter + g_SphereRadius * vertexes[i + 3].NormalL; } output.Append(vertexes[0]); output.Append(vertexes[3]); output.Append(vertexes[5]); output.RestartStrip(); output.Append(vertexes[3]); output.Append(vertexes[4]); output.Append(vertexes[5]); output.RestartStrip(); output.Append(vertexes[5]); output.Append(vertexes[4]); output.Append(vertexes[2]); output.RestartStrip(); output.Append(vertexes[3]); output.Append(vertexes[1]); output.Append(vertexes[4]); }
因为v3, v4, v5也须要在球面上,咱们还须要额外知道球的半径和球心位置。虽说经过三角形三个顶点位置和法向量能够算出圆心和半径,但直接从常量缓冲区提供这两个信息会更方便一些。
要计算诸如v3顶点所在位置,咱们能够先求出它的法向量,将v0和v1的法向量相加取其单位向量即为v3的法向量,而后从圆心开始加上半径长度的法向量便可获得顶点v3的位置。
剩下绘制圆的着色器代码以下:
// Sphere_VS.hlsl #include "Basic.hlsli" VertexPosHWNormalColor VS(VertexPosNormalColor vIn) { VertexPosHWNormalColor vOut; matrix viewProj = mul(g_View, g_Proj); float4 posW = mul(float4(vIn.PosL, 1.0f), g_World); vOut.PosH = mul(posW, viewProj); vOut.PosW = posW.xyz; vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose); vOut.Color = vIn.Color; return vOut; }
// Sphere_PS.hlsl #include "Basic.hlsli" float4 PS(VertexPosHWNormalColor pIn) : SV_Target { // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 顶点指向眼睛的向量 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); // 初始化为0 float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f); float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f); float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f); // 只计算方向光 ComputeDirectionalLight(g_Material, g_DirLight[0], pIn.NormalW, toEyeW, ambient, diffuse, spec); return pIn.Color * (ambient + diffuse) + spec; }
如今着色器的建立按绘制类别进行分组:
bool BasicEffect::InitAll(ID3D11Device * device) { if (!device) return false; if (!pImpl->m_pCBuffers.empty()) return true; if (!RenderStates::IsInit()) throw std::exception("RenderStates need to be initialized first!"); const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = { { 0, "POSITION", 0, 0, 3, 0 }, { 0, "COLOR", 0, 0, 4, 0 } }; const D3D11_SO_DECLARATION_ENTRY posNormalColorLayout[3] = { { 0, "POSITION", 0, 0, 3, 0 }, { 0, "NORMAL", 0, 0, 3, 0 }, { 0, "COLOR", 0, 0, 4, 0 } }; UINT stridePosColor = sizeof(VertexPosColor); UINT stridePosNormalColor = sizeof(VertexPosNormalColor); ComPtr<ID3DBlob> blob; // ****************** // 流输出分裂三角形 // HR(CreateShaderFromFile(L"HLSL\\TriangleSO_VS.cso", L"HLSL\\TriangleSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTriangleSOVS.GetAddressOf())); // 建立顶点输入布局 HR(device->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexPosColorLayout.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\TriangleSO_GS.cso", L"HLSL\\TriangleSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout), &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, pImpl->m_pTriangleSOGS.GetAddressOf())); // ****************** // 绘制分形三角形 // HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTriangleVS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pTrianglePS.GetAddressOf())); // ****************** // 流输出分形球体 // HR(CreateShaderFromFile(L"HLSL\\SphereSO_VS.cso", L"HLSL\\SphereSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSphereSOVS.GetAddressOf())); // 建立顶点输入布局 HR(device->CreateInputLayout(VertexPosNormalColor::inputLayout, ARRAYSIZE(VertexPosNormalColor::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexPosNormalColorLayout.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\SphereSO_GS.cso", L"HLSL\\SphereSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posNormalColorLayout, ARRAYSIZE(posNormalColorLayout), &stridePosNormalColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, pImpl->m_pSphereSOGS.GetAddressOf())); // ****************** // 绘制球体 // HR(CreateShaderFromFile(L"HLSL\\Sphere_VS.cso", L"HLSL\\Sphere_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSphereVS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\Sphere_PS.cso", L"HLSL\\Sphere_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSpherePS.GetAddressOf())); // ****************** // 流输出分形雪花 // HR(CreateShaderFromFile(L"HLSL\\SnowSO_VS.cso", L"HLSL\\SnowSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowSOVS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\SnowSO_GS.cso", L"HLSL\\SnowSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout), &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, pImpl->m_pSnowSOGS.GetAddressOf())); // ****************** // 绘制雪花 // HR(CreateShaderFromFile(L"HLSL\\Snow_VS.cso", L"HLSL\\Snow_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowVS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\Snow_PS.cso", L"HLSL\\Snow_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pSnowPS.GetAddressOf())); // ****************** // 绘制法向量 // HR(CreateShaderFromFile(L"HLSL\\Normal_VS.cso", L"HLSL\\Normal_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalVS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\Normal_GS.cso", L"HLSL\\Normal_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalGS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\Normal_PS.cso", L"HLSL\\Normal_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf())); HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pNormalPS.GetAddressOf())); pImpl->m_pCBuffers.assign({ &pImpl->m_CBFrame, &pImpl->m_CBOnResize, &pImpl->m_CBRarely}); // 建立常量缓冲区 for (auto& pBuffer : pImpl->m_pCBuffers) { HR(pBuffer->CreateBuffer(device)); } return true; }
因为新增了流输出的阶段,这里开始接下来的每个用于绘制的方法都须要把流输出绑定的顶点缓冲区都解除绑定。
void BasicEffect::SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer* nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pTriangleVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(pImpl->m_pTrianglePS.Get(), nullptr, 0); }
void BasicEffect::SetRenderSplitedSnow(ID3D11DeviceContext * deviceContext) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer* nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pSnowVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(pImpl->m_pSnowPS.Get(), nullptr, 0); }
void BasicEffect::SetRenderSplitedSphere(ID3D11DeviceContext * deviceContext) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer* nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get()); deviceContext->VSSetShader(pImpl->m_pSphereVS.Get(), nullptr, 0); deviceContext->GSSetShader(nullptr, nullptr, 0); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(pImpl->m_pSpherePS.Get(), nullptr, 0); }
为了简化设置,这里还须要提供额外的输入缓冲区和输出缓冲区。为了防止出现顶点缓冲区同时被绑定到输入装配和流输出阶段的状况,须要先清空流输出绑定的顶点缓冲区,而后将用于输入的顶点缓冲区绑定到输入装配阶段,最后才是把输出的顶点缓冲区绑定到流输出阶段。
void BasicEffect::SetStreamOutputSplitedTriangle(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer * nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetInputLayout(nullptr); deviceContext->SOSetTargets(0, nullptr, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pTriangleSOVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pTriangleSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(nullptr, nullptr, 0); }
注意这里是用LineList而不是LineStrip方式。
void BasicEffect::SetStreamOutputSplitedSnow(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosColor); UINT offset = 0; ID3D11Buffer * nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get()); deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pSnowSOVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pSnowSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(nullptr, nullptr, 0); }
void BasicEffect::SetStreamOutputSplitedSphere(ID3D11DeviceContext * deviceContext, ID3D11Buffer * vertexBufferIn, ID3D11Buffer * vertexBufferOut) { // 先恢复流输出默认设置,防止顶点缓冲区同时绑定在输入和输出阶段 UINT stride = sizeof(VertexPosNormalColor); UINT offset = 0; ID3D11Buffer * nullBuffer = nullptr; deviceContext->SOSetTargets(1, &nullBuffer, &offset); deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get()); deviceContext->IASetVertexBuffers(0, 1, &vertexBufferIn, &stride, &offset); deviceContext->VSSetShader(pImpl->m_pSphereSOVS.Get(), nullptr, 0); deviceContext->GSSetShader(pImpl->m_pSphereSOGS.Get(), nullptr, 0); deviceContext->SOSetTargets(1, &vertexBufferOut, &offset); deviceContext->RSSetState(nullptr); deviceContext->PSSetShader(nullptr, nullptr, 0); }
时隔多月,是时候请回该方法了。
这是一个惟一不须要形参就能绘制的API,它能够根据输入装配阶段绑定的缓冲区(内部存有图元数目的记录)自动进行绘制,它会通过顶点着色阶段并一直到流输出阶段。它可能会继续通过光栅化阶段到输出合并阶段,也能够不通过。
可是它的调用要求以下:
D3D11_BIND_VERTEX_BUFFER
和D3D11_BIND_STREAM_OUTPUT
关于最后一点,通常的顶点缓冲区是不会存在内部记录的。一般要求第一次流输出绘制时使用Draw
、DrawIndexed
系列的方法,而不是DrawAuto
来绘制,这样流输出缓冲区在产生运行结果的同时,其内部还会产生图元数目的记录。这样在后续的调用中,你就能够将该流输出缓冲区做为输入,而后提供另外一个流输出缓冲区做为输出,最后调用DrawAuto
来正确绘制了。
首先咱们只须要给1阶的顶点缓冲区使用指定三角形的三个顶点,而后后续阶数的顶点缓冲区就根据上一阶产出的顶点缓冲区进行"绘制"。通过6次Draw
方法的调用后,里面的7个顶点缓冲区都应该被初始化完毕,后续绘制的时候只须要直接绑定某一个顶点缓冲区到输入便可。
注意顶点缓冲区在建立的时候必定要加上D3D11_BIND_STREAM_OUTPUT
标签。
void GameApp::ResetSplitedTriangle() { // ****************** // 初始化三角形 // // 设置三角形顶点 VertexPosColor vertices[] = { { XMFLOAT3(-1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }, { XMFLOAT3(0.0f * 3, 0.866f * 3, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, { XMFLOAT3(1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) } }; // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_DEFAULT; // 这里须要容许流输出阶段经过GPU写入 vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT; // 须要额外添加流输出标签 vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); // 三角形顶点数 m_InitVertexCounts = 3; // 初始化全部顶点缓冲区 for (int i = 1; i < 7; ++i) { vbd.ByteWidth *= 3; HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf())); m_BasicEffect.SetStreamOutputSplitedTriangle(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get()); // 第一次绘制须要调用通常绘制指令,以后就可使用DrawAuto了 if (i == 1) { m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0); } else { m_pd3dImmediateContext->DrawAuto(); } } }
因为绘制方式统一用LineList,初始阶段应当提供3条线段的6个顶点,虽说每一个顶点都被重复使用了2次。
void GameApp::ResetSplitedSnow() { // ****************** // 雪花分形从初始化三角形开始,须要6个顶点 // // 设置三角形顶点 float sqrt3 = sqrt(3.0f); VertexPosColor vertices[] = { { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) } }; // 将三角形宽度和高度都放大3倍 for (VertexPosColor& v : vertices) { v.pos.x *= 3; v.pos.y *= 3; } // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_DEFAULT; // 这里须要容许流输出阶段经过GPU写入 vbd.ByteWidth = sizeof vertices; vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT; // 须要额外添加流输出标签 vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices; HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); // 顶点数 m_InitVertexCounts = 6; // 初始化全部顶点缓冲区 for (int i = 1; i < 7; ++i) { vbd.ByteWidth *= 4; HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf())); m_BasicEffect.SetStreamOutputSplitedSnow(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get()); // 第一次绘制须要调用通常绘制指令,以后就可使用DrawAuto了 if (i == 1) { m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0); } else { m_pd3dImmediateContext->DrawAuto(); } } }
这里不使用Geometry
类来构造一阶圆球,而是仅提供与外接正方体相交的六个顶点,包含八个三角形对应的24个顶点。
void GameApp::ResetSplitedSphere() { // ****************** // 分形球体 // VertexPosNormalColor basePoint[] = { { XMFLOAT3(0.0f, 2.0f, 0.0f), XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(2.0f, 0.0f, 0.0f), XMFLOAT3(1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(0.0f, 0.0f, 2.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(-2.0f, 0.0f, 0.0f), XMFLOAT3(-1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(0.0f, 0.0f, -2.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, { XMFLOAT3(0.0f, -2.0f, 0.0f), XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }, }; int indices[] = { 0, 2, 1, 0, 3, 2, 0, 4, 3, 0, 1, 4, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 1, 5 }; std::vector<VertexPosNormalColor> vertices; for (int pos : indices) { vertices.push_back(basePoint[pos]); } // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_DEFAULT; // 这里须要容许流输出阶段经过GPU写入 vbd.ByteWidth = (UINT)(vertices.size() * sizeof(VertexPosNormalColor)); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT; // 须要额外添加流输出标签 vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData, sizeof(InitData)); InitData.pSysMem = vertices.data(); HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); //#if defined(DEBUG) | defined(_DEBUG) // ComPtr<IDXGraphicsAnalysis> graphicsAnalysis; // HR(DXGIGetDebugInterface1(0, __uuidof(graphicsAnalysis.Get()), reinterpret_cast<void**>(graphicsAnalysis.GetAddressOf()))); // graphicsAnalysis->BeginCapture(); //#endif // 顶点数 m_InitVertexCounts = 24; // 初始化全部顶点缓冲区 for (int i = 1; i < 7; ++i) { vbd.ByteWidth *= 4; HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[i].ReleaseAndGetAddressOf())); m_BasicEffect.SetStreamOutputSplitedSphere(m_pd3dImmediateContext.Get(), m_pVertexBuffers[i - 1].Get(), m_pVertexBuffers[i].Get()); // 第一次绘制须要调用通常绘制指令,以后就可使用DrawAuto了 if (i == 1) { m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0); } else { m_pd3dImmediateContext->DrawAuto(); } } }
同理,在进行正常绘制的时候,因为索引1到6的顶点缓冲区内部都记录了图元数目,也能够直接用DrawAuto
方法绘制到屏幕上。
void GameApp::DrawScene() { assert(m_pd3dImmediateContext); assert(m_pSwapChain); m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black)); m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); // 根据当前绘制模式设置须要用于渲染的各项资源 if (m_ShowMode == Mode::SplitedTriangle) { m_BasicEffect.SetRenderSplitedTriangle(m_pd3dImmediateContext.Get()); } else if (m_ShowMode == Mode::SplitedSnow) { m_BasicEffect.SetRenderSplitedSnow(m_pd3dImmediateContext.Get()); } else if (m_ShowMode == Mode::SplitedSphere) { m_BasicEffect.SetRenderSplitedSphere(m_pd3dImmediateContext.Get()); } // 设置线框/面模式 if (m_IsWireFrame) { m_pd3dImmediateContext->RSSetState(RenderStates::RSWireframe.Get()); } else { m_pd3dImmediateContext->RSSetState(nullptr); } // 应用常量缓冲区的变动 m_BasicEffect.Apply(m_pd3dImmediateContext.Get()); // 除了索引为0的缓冲区缺乏内部图元数目记录,其他均可以使用DrawAuto方法 if (m_CurrIndex == 0) { m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0); } else { m_pd3dImmediateContext->DrawAuto(); } // 绘制法向量 if (m_ShowNormal) { m_BasicEffect.SetRenderNormal(m_pd3dImmediateContext.Get()); m_BasicEffect.Apply(m_pd3dImmediateContext.Get()); // 除了索引为0的缓冲区缺乏内部图元数目记录,其他均可以使用DrawAuto方法 if (m_CurrIndex == 0) { m_pd3dImmediateContext->Draw(m_InitVertexCounts, 0); } else { m_pd3dImmediateContext->DrawAuto(); } } // ****************** // 绘制Direct2D部分 // if (m_pd2dRenderTarget != nullptr) { m_pd2dRenderTarget->BeginDraw(); std::wstring text = L"切换分形:Q-三角形(面/线框) W-雪花(线框) E-球(面/线框)\n" L"主键盘数字1 - 7:分形阶数,越高越精细\n" L"M-面/线框切换\n\n" L"当前阶数: " + std::to_wstring(m_CurrIndex + 1) + L"\n" "当前分形: "; if (m_ShowMode == Mode::SplitedTriangle) text += L"三角形"; else if (m_ShowMode == Mode::SplitedSnow) text += L"雪花"; else text += L"球"; if (m_IsWireFrame) text += L"(线框)"; else text += L"(面)"; if (m_ShowMode == Mode::SplitedSphere) { if (m_ShowNormal) text += L"(N-关闭法向量)"; else text += L"(N-开启法向量)"; } m_pd2dRenderTarget->DrawTextW(text.c_str(), (UINT32)text.length(), m_pTextFormat.Get(), D2D1_RECT_F{ 0.0f, 0.0f, 600.0f, 200.0f }, m_pColorBrush.Get()); HR(m_pd2dRenderTarget->EndDraw()); } HR(m_pSwapChain->Present(0, 0)); }
如今来看一下动图感觉一些这些酷炫的效果吧:
因为文件大小限制,这里分红两个部分:
下面是带法向量的:
该项目使用图形调试器并退出的时候,会引起内存泄漏,而具体的泄漏对象估计是ID3D11Query
,然而我也没有办法直接拿到该接口对象来释放。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。