DirectX11 With Windows SDK--15 幾何着色器初探

前言

從這一部分開始,感受就像是踏入了無人深空同樣,在以前初學DX11的時候,這部份內容都是基本上跳過的,如今打算從新認真地把它給拾回來。html

DirectX11 With Windows SDK完整目錄git

Github項目源碼github

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。編程

幾何着色器

首先用一張圖來回顧一下渲染管線的各個階段,目前爲止咱們接觸的着色器有頂點着色器和像素着色器,而接觸到的渲染管線階段有:輸入裝配階段、頂點着色階段、光柵化階段、像素着色階段、輸出合併階段。數組

能夠看到,幾何着色器是咱們在將頂點送入光柵化階段以前,能夠操做頂點的最後一個階段。它一樣也容許咱們編寫本身的着色器代碼。幾何着色器能夠作以下事情:app

  1. 讓程序自動決定如何在渲染管線中插入/移除幾何體;
  2. 經過流輸出階段將頂點信息再次傳遞到頂點緩衝區;
  3. 改變圖元類型(如輸入點圖元,輸出三角形圖元);

但它也有缺點,幾何着色器輸出的頂點數據極可能是有較多重複的,從流輸出拿回到頂點緩衝區的話會佔用較多的內存空間。它自己沒法輸出索引數組。ide

幾何着色階段會收到一系列表明輸入幾何體類型的頂點,而後咱們能夠自由選擇其中的這些頂點信息,而後交給流輸出對象從新解釋成新的圖元類型(或者不變),傳遞給流輸出階段或者是光柵化階段。而幾何着色器僅可以接受來自輸入裝配階段提供的頂點信息,對每一個頂點進行處理,沒法自行決定增減頂點。函數

注意:離開幾何着色器的頂點若是要傳遞給光柵化階段,須要包含有轉換到齊次裁剪座標系的座標信息(語義爲SV_POSITIONfloat4向量)性能

可編程的幾何着色器

從一個看似沒什麼用的幾何着色器代碼入手

若咱們直接從VS項目新建一個幾何着色器文件,則能夠看到下面的代碼:字體

struct GSOutput
{
    float4 pos : SV_POSITION;
};

[maxvertexcount(3)]
void main(
    triangle float4 input[3] : SV_POSITION, 
    inout TriangleStream< GSOutput > output
)
{
    for (uint i = 0; i < 3; i++)
    {
        GSOutput element;
        element.pos = input[i];
        output.Append(element);
    }
}

ps. 可能有些人會對void main的寫法表示不爽,好比說我。不過這不是C語言的主函數......

若在輸入裝配階段指定使用TriangleList圖元的話,初步觀察該代碼,實際上你能夠發現其實該着色器只是把輸入的頂點按原樣輸出給流輸出對象,即跟什麼都沒作(鹹魚)有什麼區別。。不過從這份代碼裏面就已經包含了幾何着色器所特有的絕大部分語法了。

首先,幾何着色器是根據圖元類型來進行調用的,若使用的是TriangleList,則每個三角形的三個頂點都會做爲輸入,觸發幾何着色器的調用。這樣一個TriangleList解釋的30個頂點會觸發10次調用。

對於幾何着色器,咱們必需要指定它每次調用所容許輸出的最大頂點數目。咱們可使用屬性語法來強行修改着色器行爲:

[maxvertexcount(N)]

這裏N就是每次調用容許產出的最大頂點數目,而後最終輸出的頂點數目不會超過N的值。maxvertexcount的值應當儘量的小。

關於性能上的表現,我根據龍書提供的引用找到了對應的說明文檔:

NVIDIA08

雖然是10年前的文檔,這裏說到:在GeForce 8800 GTX,一個幾何着色器的調用在輸出1到20個標量的時候能夠達到最大運行性能表現,可是當咱們指定最大容許輸出標量的數目在27到40個時,性能僅達到峯值的50%。好比說,若是頂點的聲明以下:

struct V0
{
    float3 pos : POSITION;
    float2 tex : TEXCOORD;
};

這裏每一個頂點就已經包含了5個標量了,若是以它做爲輸出類型,則maxvertexcount爲4的時候就能夠達到理論上的峯值性能(20個標量)。

但若是頂點類型中還包含有float3類型的法向量,每一個頂點就額外包含了3個標量,這樣在maxvertexcount爲4的時候就輸出了32個標量,只有50%的峯值性能表現。

這份文檔已經將近10年了,對於那時候的顯卡來講使用幾何着色器可能不是一個很好的選擇,不過當初的顯卡也早已不能和如今的顯卡相提並論了。

注意:

  1. maxvertexcount的值應當設置到儘量小的值,由於它將直接決定幾何着色器的運行效率。
  2. 幾何着色器的每次調用最多隻能處理1024個標量,對於只包含4D位置向量的頂點來講也只能處理256個頂點。
  3. 幾何着色器輸入的結構體類型不容許超過128個標量,對於只包含4D位置向量的頂點來講也只能包含32個頂點。

在HLSL編譯器裏,若是設置的maxvertexcount過大,會直接收到編譯錯誤:

而後代碼中的triangle是用於指定輸入的圖元類型,具體支持的關鍵字以下:

圖元類型 描述
point Point list
line Line list or line strip
triangle Triangle list or triangle strip
lineadj Line list with adjacency or line strip with adjacency
triangleadj Triangle list with adjacency or triangle strip with adjacency

具體的圖元類型能夠到第2章回顧:點擊此處

而參數類型能夠是用戶自定義的結構體類型,或者是向量(float4)類型。從頂點着色器傳過來的頂點至少會包含一個表示齊次裁剪座標的向量。

參數名inupt實際上用戶是能夠任意指定的。

對於該輸入參數的元素數目,取決於前面聲明的圖元類型:

圖元類型 元素數目
point [1] 每次只能處理1個頂點
line [2] 一個線段必須包含2個頂點
triangle [3] 一個三角形須要3個頂點
lineadj [4] 一個鄰接線段須要4個頂點
triangleadj [6] 一個鄰接三角形須要6個頂點

而第二個參數必須是一個流輸出對象,並且須要被指定爲inout可讀寫類型。能夠看到,它是一個類模板,模板的形參指定要輸出的類型。流輸出對象有以下三種:

流輸出對象類型 描述
PointStream 一系列點的圖元
LineStream 一系列線段的圖元
TriangleStream 一系列三角形的圖元

流輸出對象都具備下面兩種方法:

方法 描述
Append 向指定的流輸出對象添加一個輸出的數據
RestartStrip 在以線段或者三角形做爲圖元的時候,默認是以strip的形式輸出的,
若是咱們不但願下一個輸出的頂點與以前的頂點構成新圖元,則須要
調用此方法來從新開始新的strip。若但願輸出的圖元類型也保持和原
來同樣的TriangleList,則須要每調用3次Append方法後就調用一次
RestartStrip。

注意:

  1. 所謂的刪除頂點,實際上就是不將該頂點傳遞給流輸出對象
  2. 若傳入的頂點中多餘的部分沒法構成對應的圖元,則拋棄掉這些多餘的頂點

在開始前,先放出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_EyePosW;
    float g_CylinderHeight;
}


struct VertexPosColor
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};

struct VertexPosHColor
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};

實戰1: 將一個三角形分割成三個三角形

如今咱們的目標是把一個三角形分裂成三個三角形:

這也爲之後實現分形作爲基礎。建議讀者能夠先自行嘗試編寫着色器代碼再來對比。在編寫好着色器代碼後,
要給渲染管線綁定好一切所需的資源纔可以看到效果。

HLSL代碼

Triangle_VS.hlsl, Triangle_GS.hlslTriangle_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_GS.hlsl
#include "Basic.hlsli"

[maxvertexcount(9)]
void GS(triangle VertexPosHColor input[3], inout TriangleStream<VertexPosHColor> output)
{
    //
    // 將一個三角形分裂成三個三角形,即沒有v3v4v5的三角形
    //       v1
    //       /\
    //      /  \
    //   v3/____\v4
    //    /\xxxx/\
    //   /  \xx/  \
    //  /____\/____\
    // v0    v5    v2


    VertexPosHColor 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].PosH = (input[i].PosH + input[(i + 1) % 3].PosH) / 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_PS.hlsl
#include "Basic.hlsli"

float4 PS(VertexPosHColor pIn) : SV_Target
{
    return pIn.Color;
}

這裏輸入和輸出的圖元類型都是一致的,但不管什麼狀況都必定要注意設置好maxvertexcount的值,這裏固定一個三角形的三個頂點輸出9個頂點(構成三個三角形),而且每3次Append就須要調用1次RestartStrip

實戰2: 經過圓線構造圓柱體側面

已知圖元類型爲LineStrip,如今有一系列連續的頂點構成圓線(近似圓弧的連續折線),構造出圓柱體的側面。即輸入圖元類型爲線段,輸出一個矩形(兩個三角形)。

思路: 光有頂點位置還不足以構造出圓柱體側面,由於沒法肯定圓柱往哪一個方向延伸。因此咱們還須要對每一個頂點引入所在圓柱側面的法向量,經過叉乘就能夠肯定上方向/下方向並進行延伸了。

HLSL代碼

Cylinder_VS.hlsl, Cylinder_GS.hlslCylinder_PS.hlsl的實現以下:

// Cylinder_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;
}
// Cylinder_GS.hlsl
#include "Basic.hlsli"

// 一個v0v1線段輸出6個三角形頂點
[maxvertexcount(6)]
void GS(line VertexPosHWNormalColor input[2], inout TriangleStream<VertexPosHWNormalColor> output)
{
    // *****************************
    // 要求圓線是順時針的,而後自底向上構造圓柱側面           
    //   -->      v2____v3
    //  ______     |\   |
    // /      \    | \  |
    // \______/    |  \ |
    //   <--       |___\|
    //           v1(i1) v0(i0)

    float3 upDir = normalize(cross(input[0].NormalW, (input[1].PosW - input[0].PosW)));
    VertexPosHWNormalColor v2, v3;
    
    matrix viewProj = mul(g_View, g_Proj);


    v2.PosW = input[1].PosW + upDir * g_CylinderHeight;
    v2.PosH = mul(float4(v2.PosW, 1.0f), viewProj);
    v2.NormalW = input[1].NormalW;
    v2.Color = input[1].Color;

    v3.PosW = input[0].PosW + upDir * g_CylinderHeight;
    v3.PosH = mul(float4(v3.PosW, 1.0f), viewProj);
    v3.NormalW = input[0].NormalW;
    v3.Color = input[0].Color;

    output.Append(input[0]);
    output.Append(input[1]);
    output.Append(v2);
    output.RestartStrip();

    output.Append(v2);
    output.Append(v3);
    output.Append(input[0]);
}
// Cylinder_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;
}

實戰3: 畫出頂點的法向量

畫出頂點的法向量能夠幫助你進行調試,排查法向量是否出現了問題。這時候圖元的類型爲PointList,須要經過幾何着色器輸出一個線段(兩個頂點)。因爲頂點中包含法向量,剩下的就是要自行決定法向量的長度。

下圖的法向量長度爲0.5

HLSL代碼

Normal_VS.hlsl, Normal_GS.hlslNormal_PS.hlsl的實現以下:

// Normal_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;
}
// Normal_GS.hlsl
#include "Basic.hlsli"

[maxvertexcount(2)]
void GS(point VertexPosHWNormalColor input[1], inout LineStream<VertexPosHWNormalColor> output)
{
    matrix viewProj = mul(g_View, g_Proj);
    

    VertexPosHWNormalColor v;

    // 防止資源爭奪
    v.PosW = input[0].PosW + input[0].NormalW * 0.01f;
    v.NormalW = input[0].NormalW;
    v.PosH = mul(float4(v.PosW, 1.0f), viewProj);
    v.Color = input[0].Color;
    output.Append(v);

    v.PosW = v.PosW + input[0].NormalW * 0.5f;
    v.PosH = mul(float4(v.PosW, 1.0f), viewProj);

    output.Append(v);
}
// Normal_PS.hlsl
#include "Basic.hlsli"

float4 PS(VertexPosHWNormalColor pIn) : SV_TARGET
{
    return pIn.Color;
}

C++代碼的部分變化

BasicEffect的變化

變化以下:

class BasicEffect : public IEffect
{
public:

    BasicEffect();
    virtual ~BasicEffect() override;

    BasicEffect(BasicEffect&& moveFrom) noexcept;
    BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;

    // 獲取單例
    static BasicEffect& Get();

    

    // 初始化Basic.hlsli所需資源並初始化渲染狀態
    bool InitAll(ID3D11Device * device);


    //
    // 渲染模式的變動
    //

    // 繪製三角形分裂
    void SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext);
    // 繪製無上下蓋的圓柱體
    void SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext);
    // 繪製全部頂點的法向量
    void SetRenderNormal(ID3D11DeviceContext * deviceContext);
    

    //
    // 矩陣設置
    //

    void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);
    void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);
    void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);

    
    //
    // 光照、材質和紋理相關設置
    //

    // 各類類型燈光容許的最大數目
    static const int maxLights = 5;

    void SetDirLight(size_t pos, const DirectionalLight& dirLight);
    void SetPointLight(size_t pos, const PointLight& pointLight);
    void SetSpotLight(size_t pos, const SpotLight& spotLight);

    void SetMaterial(const Material& material);



    void XM_CALLCONV SetEyePos(DirectX::FXMVECTOR eyePos);

    // 設置圓柱體側面高度
    void SetCylinderHeight(float height);

    // 應用常量緩衝區和紋理資源的變動
    void Apply(ID3D11DeviceContext * deviceContext);
    
private:
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

BasicEffect::SetRenderSplitedTriangle方法--渲染分裂的三角形

該方法處理的是圖元TriangleList。由於後續的方法處理的圖元不一樣,在調用開始就得設置回正確的圖元。也請確保輸入裝配階段提供好須要分裂的三角形頂點。

void BasicEffect::SetRenderSplitedTriangle(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexPosColorLayout.Get());
    deviceContext->VSSetShader(pImpl->m_pTriangleVS.Get(), nullptr, 0);
    deviceContext->GSSetShader(pImpl->m_pTriangleGS.Get(), nullptr, 0);
    deviceContext->RSSetState(nullptr);
    deviceContext->PSSetShader(pImpl->m_pTrianglePS.Get(), nullptr, 0);

}

BasicEffect::SetRenderCylinderNoCap方法--渲染圓柱側面

該方法處理的是圖元LineStrip,確保輸入的一系列頂點和法向量可以在同一平面上。若提供的頂點集合按順時針排布,則會自底向上構建出圓柱體,反之則是自頂向下構建。

這裏須要關閉背面裁剪,由於咱們也能夠看到圓柱側面的內部(沒有蓋子)。

void BasicEffect::SetRenderCylinderNoCap(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP);
    deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get());
    deviceContext->VSSetShader(pImpl->m_pCylinderVS.Get(), nullptr, 0);
    deviceContext->GSSetShader(pImpl->m_pCylinderGS.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    deviceContext->PSSetShader(pImpl->m_pCylinderPS.Get(), nullptr, 0);

}

BasicEffect::SetRenderNormal方法--渲染法向量

該方法處理的圖元是PointList,確保輸入的頂點要包含法向量。

void BasicEffect::SetRenderNormal(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexPosNormalColorLayout.Get());
    deviceContext->VSSetShader(pImpl->m_pNormalVS.Get(), nullptr, 0);
    deviceContext->GSSetShader(pImpl->m_pNormalGS.Get(), nullptr, 0);
    deviceContext->RSSetState(nullptr);
    deviceContext->PSSetShader(pImpl->m_pNormalPS.Get(), nullptr, 0);

}

GameApp類的變化

該項目包含上面三種實戰內容,須要用戶去指定當前播放的模式。

首先聲明部分變化以下:

class GameApp : public D3DApp
{
public:
    enum class Mode { SplitedTriangle, CylinderNoCap, CylinderNoCapWithNormal };
    
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();

private:
    bool InitResource();

    void ResetTriangle();
    void ResetRoundWire();



private:
    
    ComPtr<ID2D1SolidColorBrush> m_pColorBrush;                 // 單色筆刷
    ComPtr<IDWriteFont> m_pFont;                                // 字體
    ComPtr<IDWriteTextFormat> m_pTextFormat;                    // 文本格式

    ComPtr<ID3D11Buffer> m_pVertexBuffer;                       // 頂點集合
    int m_VertexCount;                                          // 頂點數目
    Mode m_ShowMode;                                            // 當前顯示模式

    BasicEffect m_BasicEffect;                                  // 對象渲染特效管理

};

GameApp::ResetTriangle方法--重設爲三角形頂點

void GameApp::ResetTriangle()
{
    // ******************
    // 初始化三角形
    //

    // 設置三角形頂點
    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_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.ReleaseAndGetAddressOf()));
    // 三角形頂點數
    m_VertexCount = 3;
}

GameApp::ResetRoundWire方法--重設爲圓線頂點

void GameApp::ResetRoundWire()
{
    // ****************** 
    // 初始化圓線
    // 設置圓邊上各頂點
    // 必需要按順時針設置
    // 因爲要造成閉環,起始點須要使用2次
    //  ______
    // /      \
    // \______/
    //

    VertexPosNormalColor vertices[41];
    for (int i = 0; i < 40; ++i)
    {
        vertices[i].pos = XMFLOAT3(cosf(XM_PI / 20 * i), -1.0f, -sinf(XM_PI / 20 * i));
        vertices[i].normal = XMFLOAT3(cosf(XM_PI / 20 * i), 0.0f, -sinf(XM_PI / 20 * i));
        vertices[i].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
    }
    vertices[40] = vertices[0];

    // 設置頂點緩衝區描述
    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.ReleaseAndGetAddressOf()));
    // 線框頂點數
    m_VertexCount = 41;
}

GameApp類剩餘部分能夠在項目源碼中查看。

最終效果以下:

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索