到這裏計算着色器的主線學習基本結束,剩下的就是再補充兩個有關圖像處理方面的應用。這裏麪包含了龍書11的圖像模糊,以及龍書12額外提到的Sobel算子進行邊緣檢測。主要內容源自於龍書12,項目源碼也基於此進行調整。html
學習目標:git
DirectX11 With Windows SDK完整目錄github
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。數組
在圖像處理中,常常須要用到卷積,不少效果都可以經過卷積的形式來實現。針對源圖像中的每個像素\(P_{ij}\),計算以它爲中心的m×n矩陣的加權值。此加權值即是通過處理後圖像中第i行、第j列的顏色,若是寫成卷積的形式則爲:
\[ H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i-r,j-c} \]
其中,\(m=2a+1\)且\(n=2b+1\),將m與n強制爲奇數,以此來保證m×n矩陣老是具備「中心」項。若a=b=r,則只需指定半徑r就能夠肯定矩陣的大小。\(W_{rc}\)爲m×n矩陣(又稱內核、算子)中的權值。爲了方便觀察計算及編碼,一般會將內核旋轉180°,這樣就獲得了更加經常使用的計算公式:
\[ H_{ij}=\sum_{r=-a}^{a}\sum_{c=-b}^{b}W_{rc}P_{i+r,j+c} \]
若內核的全部權值的和爲1,則它能夠用來作模糊處理;若是權值和大於0小於1,則處理後的圖像會隨着顏色的缺失而變暗;若是權值和大於1,則處理後的圖像會隨着顏色的增添而更加明亮。固然也會有權值和等於0甚至可能小於0的狀況,好比索貝爾算子。app
在保證權值和爲1的前提下,咱們就能用多種不一樣的方法來計算它。其中就有一種廣爲人知的模糊運算:高斯模糊(Gaussian blur)。該算法藉助高斯函數\(G(x)=exp(-\frac{x^2}{2\sigma^2})\)來獲取權值。下圖展現了取不一樣σ值時高斯函數的對應圖像:函數
能夠看到,若σ越大,則曲線越趨於平緩,給鄰近點所賦予的權值也就越大。
\[ G(x)=exp(-\frac{x^2}{2\sigma^2})=e^{-\frac{x^2}{2\sigma^2}} \]
若是學過幾率論的話應該知道它很像標準正態分佈的機率密度,只不過缺了一個係數\(\frac{1}{\sqrt{2\pi}\;\sigma}\)。性能
如今假設咱們要進行規模爲1×5的高斯模糊(即在水平方向進行1D模糊),且設σ=1。分別對x=-2,-1,0,1,2求G(x)的值,能夠獲得:
\[ \begin{align}G(-2)&=exp(-\frac{(-2)^2}{2})=e^{-2} \\G(-1)&=exp(-\frac{(-1)^2}{2})=e^{-\frac{1}{2}} \\G(0)&=exp(0)=1 \\G(1)&=exp(-\frac{1^2}{2})=e^{-\frac{1}{2}} \\G(2)&=exp(-\frac{2^2}{2})=e^{-2}\end{align} \]
可是,這些數據還不是最終的權值,由於它們的和不爲1:
\[ \begin{align} \sum_{x=-2}^{x=2}G(x)&=G(-2)+G(-1)+G(0)+G(1)+G(2)\\ &=1+2e^{-\frac{1}{2}}+2e^{-2}\\ &\approx 2.48373 \end{align} \]
若是將前面5個值都除以它們的和進行規格化處理,那麼咱們便會基於高斯函數得到總和爲1的各個權值:
\[ \begin{align} w_{-2}&=\frac{G(-2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\ w_{-1}&=\frac{G(-1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\ w_{0}&=\frac{G(0)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.4026\\ w_{1}&=\frac{G(1)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.2442\\ w_{2}&=\frac{G(2)}{\sum_{x=-2}^{x=2}G(x)}\approx 0.0545\\ \end{align} \]
對於二維的高斯函數,有
\[ \begin{align} G(x, y) &= G(x)\cdot G(y) \\ &=exp(-\frac{x^2}{2\sigma^2})\cdot exp(-\frac{x^2}{2\sigma^2}) \\ &=e^{-\frac{x^2+y^2}{2\sigma^2}} \end{align} \]
假如咱們要進行3x3的高斯模糊,且設σ=1,則未通過歸一化的內核爲:
\[ \begin{bmatrix} G(-1)G(-1) & G(-1)G(0) & G(-1)G(1) \\ G(0)G(-1) & G(0)G(0) & G(0)G(1) \\ G(1)G(-1) & G(1)G(0) & G(1)G(1) \\ \end{bmatrix} = \begin{bmatrix} G(-1) \\ G(0) \\ G(1) \\ \end{bmatrix}\begin{bmatrix} G(-1) & G(0) & G(1) \\ \end{bmatrix} \]
因爲上面的內核矩陣能夠寫成一個列向量乘以一個行向量的形式,所以在作模糊的時候能夠將一個2D模糊過程分爲兩個1D模糊過程。這也就說明該內核具備可分離性。學習
所以有:
\[ Blur(I)=Blur_V(Blur_H(I)) \]
假如模糊核爲一個9×9矩陣,咱們就須要對總計81個樣本依次進行2D模糊運算。但經過將模糊過程分離爲兩個1D模糊階段,便僅須要處理9+9=18個樣本!咱們經常要對紋理進行模糊處理,而對紋理採樣是代價高昂的操做。所以,經過分離模糊過程來減小紋理採樣操做是一種受用戶歡迎的優化手段。儘管有些模糊方法不具有可分離性,但只要保證最終圖像在視覺上足夠精準,咱們每每仍是能以優化性能爲目的而簡化其模糊過程。優化
首先,假設所運用的模糊算法具備可分離性,據此將模糊操做分爲兩個1D模糊運算:一個橫向模糊運算,一個縱向模糊運算。假定用戶提供了一個紋理A做爲輸入(一般是做爲SRV形參),以及一個紋理B做爲輸出(一般是做爲UAV形參)。不過要考慮到有的用戶但願將直接修改紋理A,將紋理A的SRV和UAV都傳入。所以咱們仍是須要兩個存儲中間結果的紋理T0、T1,過程以下:
因爲渲染到紋理種的場景於窗口工做區要保持着相同的分辨率,咱們須要不時從新構建離屏紋理,而模糊算法用的臨時紋理T也是如此。在GameApp::OnResize
的時候從新調整便可。
假如要處理的圖像寬度爲w、寬度爲h。對於1D縱向模糊而言,一個線程組用256個線程來處理水平方向上的線段,並且每一個線程又負責圖像中一個像素的模糊操做。所以,爲了圖像中的每一個像素都能獲得模糊處理,咱們須要在x方向上調度\(ceil(\frac{w}{256})\)個線程組(ceil爲上取整函數),且在y方向上調度h個線程組。若是w不能被256整除,則最後一次調度的線程組會存有多餘的線程(見下圖)。咱們對於這種狀況無能爲力,由於線程組的大小固定。所以,咱們只得把注意力放在着色器代碼中越界問題的鉗位檢測(clamping check)上。
1D縱向模糊於上述1D橫向模糊的狀況類似。在縱向模糊過程當中,線程組就像由256個線程構成的垂直線段,每一個線程只負責圖像中一個像素的模糊運算。所以,爲了使圖像中的每一個像素都能獲得模糊處理,咱們須要在y方向上調度\(ceil(\frac{h}{256})\)個線程組,並在x方向上調度w個線程組。
如今來考慮對一個28x14像素的紋理進行處理,咱們所用的橫向、縱向線程組的規模分別爲8x1和1x8(採用X×Y的表示格式)。對於水平方向的處理過程來講,爲了處理全部的像素,咱們須要在x方向上調度\(ceil(\frac{w}{8})=ceil(\frac{28}{8})=4\)個線程組,並在y方向上調度14個線程組。因爲28並不能被8整除,因此最右側的線程組中會有\((4\times 8-28)\times 14=56\)個線程聲明都不作。對於垂直方向的處理過程而言,爲了處理全部的像素,咱們須要在y方向上分派\(ceil(\frac{h}{8})=ceil(\frac{14}{8})=2\)個線程組,並在x方向上調度28個線程組。同理,因爲14並不能被8整除,因此最下側的線程組中會有\((2\times 8 - 14)\times 28\)個閒置的線程。沿用同一思路就能夠將線程組擴展爲256個線程的規模來處理更大的紋理。
BlurFilter::Execute
不只計算出了每一個方向要調度的線程組數量,還開啓了計算着色器的模糊運算:
void BlurFilter::Execute(ID3D11DeviceContext* deviceContext, ID3D11ShaderResourceView* inputTex, ID3D11UnorderedAccessView* outputTex, UINT blurTimes) { if (!deviceContext || !inputTex || !blurTimes) return; // 設置常量緩衝區 D3D11_MAPPED_SUBRESOURCE mappedData; deviceContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData); memcpy_s(mappedData.pData, sizeof m_CBSettings, &m_CBSettings, sizeof m_CBSettings); deviceContext->Unmap(m_pConstantBuffer.Get(), 0); deviceContext->CSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf()); ID3D11UnorderedAccessView* nullUAV[1] = { nullptr }; ID3D11ShaderResourceView* nullSRV[1] = { nullptr }; // 第一次模糊 // 橫向模糊 deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0); deviceContext->CSSetShaderResources(0, 1, &inputTex); deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr); deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1); deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); // 縱向模糊 deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0); deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf()); if (blurTimes == 1 && outputTex) deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr); else deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr); deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1); deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); // 剩餘模糊次數 while (--blurTimes) { // 橫向模糊 deviceContext->CSSetShader(m_pBlurHorzCS.Get(), nullptr, 0); deviceContext->CSSetShaderResources(0, 1, m_pTempSRV1.GetAddressOf()); deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV0.GetAddressOf(), nullptr); deviceContext->Dispatch((UINT)ceilf(m_Width / 256.0f), m_Height, 1); deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); // 縱向模糊 deviceContext->CSSetShader(m_pBlurVertCS.Get(), nullptr, 0); deviceContext->CSSetShaderResources(0, 1, m_pTempSRV0.GetAddressOf()); if (blurTimes == 1 && outputTex) deviceContext->CSSetUnorderedAccessViews(0, 1, &outputTex, nullptr); else deviceContext->CSSetUnorderedAccessViews(0, 1, m_pTempUAV1.GetAddressOf(), nullptr); deviceContext->Dispatch(m_Width, (UINT)ceilf(m_Height / 256.0f), 1); deviceContext->CSSetUnorderedAccessViews(0, 1, nullUAV, nullptr); } // 解除剩餘綁定 deviceContext->CSSetShaderResources(0, 1, nullSRV); }
其他C++端源碼則直接去項目源碼看便可。
因爲水平模糊與垂直模糊的實現原理相仿,這裏咱們只討論水平模糊。
在上面的代碼中,咱們能夠看到調度的線程組是由256個線程構成的水平「線段」,每一個線程都負責圖像中一個像素的模糊操做。一種低效的實現方案是,每一個線程都簡單地計算出以正在處理的像素爲中心的行矩陣(由於咱們如今正在進行的是1D橫向模糊處理,因此要針對行矩陣進行計算)的加權平均值。這種辦法的缺點是須要屢次拾取同一紋素。
僅考慮輸入圖像中的這兩個相鄰像素,假設模糊核爲1×7。光是在對這兩個像素進行模糊的過程當中,8個不一樣的像素中就已經有6個被採集了2次,並且要考慮到訪問設備內存的效率在GPU內存模型中是屬於比較慢的一種。
咱們能夠根據前面一節提到的模糊處理策略,利用共享內存來優化上述算法。這樣一來,每一個線程就能夠在共享內存中讀取或存儲所需的紋素數據。待全部線程都從共享內存讀取到它們所需的紋素後,就可以執行模糊運算了。不得不說,從共享內存中讀取數據的速度飛快。除此以外,還有一件棘手的事情,就是利用具備n = 256個線程的線程組行模糊運算的時候,卻須要n + 2R個紋素數據,這裏的R就是模糊半徑:
因爲模糊半徑的緣由,在處理線程組邊界附近的像素時,可能會讀取線程組之外存在「越界」狀況的像素。解決辦法其實也並不複雜。咱們只須要分配出能容納n + 2R個元素的共享內存,而且有2R個線程要各獲取兩個紋素數據。惟一麻煩的地方就是在共享內存時要多花心思,由於組內線程ID此時不能於共享內存中的元素一一對應了。下圖演示了當R=4時,從線程到共享內存的映射過程。
在此例中,R = 4。最左側的4個線程以及最右側的4個線程,每一個都要讀取2個紋素數據,並將它們存於共享內存之中。而這8個線程以外的全部線程都只須要讀取1個像素,並將其存於共享內存之中。這樣一來,咱們便可以獲得以模糊半徑R對N個像素進行模糊處理所需的全部紋素數據。
如今要討論的是最後一種狀況,即下圖中所示的最左側於最右側的線程組在索引輸入圖像時會發生越界的情形。
前面提到,從越界的索引處讀取數據並非非法操做,而是返回0(對越界索引處進行寫入是不會執行任何操做的,即no-op)。然而,咱們在讀取越界數據時並不但願獲得數據0,由於這意味着值爲0的顏色(即黑色)會影響到邊界處的模糊結果。咱們此時期盼能實現出相似於鉗位(clamp)紋理尋址模式的效果,即在讀取越界的數據時,可以得到一個與邊界紋素相同的數據。這個方案能夠經過對索引進行鉗位來加以實現,在下面完整的着色器代碼能夠看到(這裏將模糊半徑調大了):
// Blur.hlsli cbuffer CBSettings : register(b0) { int g_BlurRadius; // 最多支持19個模糊權值 float w0; float w1; float w2; float w3; float w4; float w5; float w6; float w7; float w8; float w9; float w10; float w11; float w12; float w13; float w14; float w15; float w16; float w17; float w18; } Texture2D g_Input : register(t0); RWTexture2D<float4> g_Output : register(u0); static const int g_MaxBlurRadius = 9; #define N 256 #define CacheSize (N + 2 * g_MaxBlurRadius)
// Blur_Horz_CS.hlsl #include "Blur.hlsli" groupshared float4 g_Cache[CacheSize]; [numthreads(N, 1, 1)] void CS(int3 GTid : SV_GroupThreadID, int3 DTid : SV_DispatchThreadID) { // 放在數組中以便於索引 float g_Weights[19] = { w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15, w16, w17, w18 }; // 經過填寫本地線程存儲區來減小帶寬的負載。若要對N個像素進行模糊處理,根據模糊半徑, // 咱們須要加載N + 2 * BlurRadius個像素 // 此線程組運行着N個線程。爲了獲取額外的2*BlurRadius個像素,就須要有2*BlurRadius個 // 線程都多采集一個像素數據 if (GTid.x < g_BlurRadius) { // 對於圖像左側邊界存在越界採樣的狀況進行鉗位(Clamp)操做 int x = max(DTid.x - g_BlurRadius, 0); g_Cache[GTid.x] = g_Input[int2(x, DTid.y)]; } if (GTid.x >= N - g_BlurRadius) { // 對於圖像左側邊界存在越界採樣的狀況進行鉗位(Clamp)操做 // 震驚的是Texture2D竟然能經過屬性Length訪問寬高 int x = min(DTid.x + g_BlurRadius, g_Input.Length.x - 1); g_Cache[GTid.x + 2 * g_BlurRadius] = g_Input[int2(x, DTid.y)]; } // 將數據寫入Cache的對應位置 // 針對圖形邊界處的越界採樣狀況進行鉗位處理 g_Cache[GTid.x + g_BlurRadius] = g_Input[min(DTid.xy, g_Input.Length.xy - 1)]; // 等待全部線程完成任務 GroupMemoryBarrierWithGroupSync(); // 開始對每一個像素進行混合 float4 blurColor = float4(0.0f, 0.0f, 0.0f, 0.0f); for (int i = -g_BlurRadius; i <= g_BlurRadius; ++i) { int k = GTid.x + g_BlurRadius + i; blurColor += g_Weights[i + g_BlurRadius] * g_Cache[k]; } g_Output[DTid.xy] = blurColor; }
Blur_Vert_CS.hlsl
與上面的代碼相似,就再也不放出。
最右側的線程組可能存有一些多餘的線程,但輸出的紋理中並無與之對應的元素(意味着它們根本無需輸出任何數據,見上圖)。此時DTid.xy
即爲輸出紋理以外的一個越界索引。可是咱們無需爲此而擔憂,由於向越界處寫入數據的效果是不進行任何操做(no-op)。
索貝爾算子(Sobel Operator)用於圖像的邊緣檢測。它會針對每個像素估算其梯度(gradient)的大小。梯度值較大的像素則代表它與周圍像素的顏色差別極大,於是此像素必定位於圖像的邊緣。相反,具備較小梯度的像素則意味着它與臨近像素的顏色趨同,即該像素並不處於圖像邊沿之上。須要注意的是,索貝爾算子返回的並不是是像素是否位於圖像邊緣的二元結果,而是一個範圍在[0.0, 1.0]內表示邊緣「陡峭」程度的灰度值:值爲0表示很是平坦,與周圍像素並無顏色差別;值爲1表示很是陡峭,與周圍像素顏色差別很大。一般索貝爾逆圖像(1-c)每每會更加直觀有效,這時白色表示平坦且不位於圖像邊緣,而黑色則表明陡峭且處於圖像邊緣。
運用索貝爾算子後的結果:
索貝爾算子的逆圖像的結果:
若是將原始圖像與其通過索貝爾算子生成的逆圖像二者間的對應顏色值相乘,咱們將得到相似於卡通畫或動漫書中那樣,其邊緣就像用黑色的筆勾描後的圖片效果。哪怕待處理的圖像首先通過模糊處理後已經隱去了部分細節,依舊能夠恢復其相對粗獷的畫風,令其邊緣清晰起來。
索貝爾算子所採用的算法是先進行加權平均,而後進行近似求導運算,計算方法以下:
\[ G_x = \Delta_x f(x, y) = [f(x-1,y+1)+2f(x,y+1)+f(x+1,y+1)]-[f(x-1,y-1)+2f(x,y-1)+f(x+1,y-1)] \\ G_y = \Delta_y f(x, y) = [f(x-1,y-1)+2f(x-1,y)+f(x-1,y+1)]-[f(x+1,y-1)+2f(x+1,y)+f(x+1,y+1)] \]
所以咱們就獲得了梯度向量\((\Delta_x f(x, y), \Delta_y f(x, y))\),而後求出它的長度\(\parallel \sqrt{G_{x}^{2} + G_{y}^{2}}\parallel\)即爲變化方向最大處的變化率。
索貝爾算子的HLSL代碼實現以下:
// Sobel_CS.hlsl Texture2D g_Input : register(t0); RWTexture2D<float4> g_Output : register(u0); // 將RGB色轉化爲灰色 float3 RGB2Gray(float3 color) { return (float3) dot(color, float3(0.299f, 0.587f, 0.114f)); } [numthreads(16, 16, 1)] void CS(int3 DTid : SV_DispatchThreadID) { // 採集當前待處理像素及相鄰的八個像素 float4 colors[3][3]; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { int2 xy = DTid.xy + int2(-1 + j, -1 + i); colors[i][j] = g_Input[xy]; } } // 針對每一個顏色通道,利用索貝爾算子估算出關於x的偏導數近似值 float4 Gx = -1.0f * colors[0][0] - 2.0f * colors[1][0] - 1.0f * colors[2][0] + 1.0f * colors[0][2] + 2.0f * colors[1][2] + 1.0f * colors[2][2]; // 針對每一個顏色通道,利用索貝爾算子估算出關於y的偏導數的近似值 float4 Gy = -1.0f * colors[2][0] - 2.0f * colors[2][1] - 1.0f * colors[2][2] + 1.0f * colors[0][0] + 2.0f * colors[0][1] + 1.0f * colors[0][2]; // 梯度向量即爲(Gx, Gy)。針對每一個顏色通道,計算出梯度大小(即梯度的模擬) // 以找到最大的變化率 float4 mag = sqrt(Gx * Gx + Gy * Gy); // 將梯度陡峭的邊緣處繪製爲黑色,梯度平坦的非邊緣處繪製爲白色 mag = 1.0f - float4(saturate(RGB2Gray(mag.xyz)), 0.0f); g_Output[DTid.xy] = mag; }
// VS使用Basic_VS_2D // Composite_PS.hlsl Texture2D g_BaseMap : register(t0); // 原紋理 Texture2D g_EdgeMap : register(t1); // 邊緣紋理 SamplerState g_SamLinearWrap : register(s0); // 線性過濾+Wrap採樣器 SamplerState g_SamPointClamp : register(s1); // 點過濾+Clamp採樣器 float4 PS(float4 posH : SV_Position, float2 tex : TEXCOORD) : SV_Target { float4 c = g_BaseMap.SampleLevel(g_SamPointClamp, tex, 0.0f); float4 e = g_EdgeMap.SampleLevel(g_SamPointClamp, tex, 0.0f); // 將原始圖片與邊緣圖相乘 return c * e; }
在C++端的代碼能夠直接去源碼中尋找SobelFilter
。
本樣例爲高斯模糊提供了調整模糊半徑、Sigma和次數的功能。模糊半徑越大,模糊次數越大,幀數會越低。若是你的電腦配置承受不住,建議關掉OIT來觀察模糊效果會更好一些。至於Sobel算子則沒法調整。
整個計算着色器的內容就到此結束了。
該項目沒法圖形調試,就和DirectX SDK Samples中OIT樣例同樣,遇到了未知問題。若是要調試,須要把OIT相關的代碼撤走才能調試。
此外,龍書12中的Composite.hlsl
頂點着色器用到了SV_VertexID
,一旦用了該系統值做爲輸入,就沒法在最終結果選擇像素觀察運行過程了。所以本項目並無使用內置於着色器的頂點數據。
對圖像進行模糊處理是一種昂貴的操做,它所花費的時間於待處理的圖像大小息息相關。通常狀況下,在把場景渲染到離屏紋理的時候,咱們一般會將離屏紋理的大小設爲後備緩衝區尺寸的1/4.也就是說,假如後備緩衝區的大小爲800x600,則離屏紋理的尺寸將爲400x300.這樣一來不只能加快離屏紋理的繪製速度(即減小了須要填充的像素數量),並且能同時提高模糊圖像的處理速度(須要模糊的像素也就更少)。另外,當紋理從1/4的屏幕分辨率拉伸爲完整大屏幕分辨率時,紋理放大過濾器也會執行一些額外的模糊操做。
如今嘗試修改項目,讓BlurFilter
的分辨率爲400x300,實現上述內容。
提示:TextureRender開啓mipmaps,並將mip等級爲1的紋理做爲SRV。
Composite_VS.hlsl
,將繪製整個屏幕的6個頂點直接放在頂點着色器中,而後只使用SV_VertexID
做爲頂點着色器的形參來繪製。研究雙邊模糊(雙邊濾波器,bilateral blur)計數,並用計算着色器加以實現。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。