Windows 8 DirectX 開發學習筆記(十五)使用Billboard實現樹木貼圖

要使用DirectX來獲得三維效果,一般首先要生成一個三維模型,然後計算它在可視空間中的投影。這樣得到的二維圖像十分真實,但是計算量也很大。在大規模場景渲染中,隨着模型精度的提高,這樣的處理方式十分消耗資源。人眼的分辨率是有限的,對於遠處的模型,模糊一些不會影響到整體效果。Billboard技術就是用二維圖片來模擬三維模型的投影,從而提高渲染效率。只要距離足夠遠,通過將二維圖片旋轉至合適角度,實際渲染效果與三維模型相差無幾,但計算量減少很多。本文使用幾何着色器,利用Billboard技術在之前的模型中添加樹木貼圖。

整個過程與上一篇的內容類似。不過這一次樹木模型的頂點結構與其他模型不同,所以要重新寫一套着色器(TreeVertexShader.hlslTreeGeometryShader.hlslTreePixelShader.hlsl)。使用Billboard繪製樹木時,CPU只要生成樹木的位置和大小即可,計算過程均由幾何着色器完成,而頂點着色器只起到傳遞參數的作用,代碼如下:

struct VertexShaderInput
{
    float3 center : POSITION;
    float2 size : SIZE;
};
 
struct VertexShaderOutput
{ 
    float3 center : POSITION;
    float2 size : SIZE;
};
 
VertexShaderOutput main( VertexShaderInput input )
{
    VertexShaderOutputoutput;
 
    output.center =input.center;
    output.size =input.size;
 
    return output;
}

另外,爲了方便觀察繪製效果,新像素着色器只進行紋理採樣,不實現光照等效果。

SamplerState samplerLinear : register(s0);
Texture2D texDiffuse : register(t0);
 
struct PixelInputType
{
    float4 posH    : SV_POSITION;
    float3 posW    : POSITION;
    float3 normalW : NORMAL;
    float2 texC    : TEXCOORD;
};
 
float4 main(PixelInputType pIn) : SV_Target
{
    float4 diffuse =texDiffuse.Sample(samplerLinear, pIn.texC);
 
    // alpha值小於0.25,放棄該像素
    clip(diffuse.a -0.25f);
   
    // 輸出紋理顏色
    return diffuse;
}

三個新着色器中,幾何着色器是重點。由於幾何着色器在頂點着色器和像素着色器之間,根據前面的代碼可以很容易地得到幾何着色器的結構定義:

struct GSInput
{
    float3 center : POSITION;
    float2 size : SIZE;
};
 
struct GSOutput
{
    float4 posH : SV_POSITION;
    float3 posW : POSITION;
    float3 normal : NORMAL;
    float2 tex : TEXCOOD;
};

而計算樹木貼圖的變換矩陣時需要觀察點的位置等信息,所以在幾何着色器中定義一個常量緩衝區來存儲相關信息:

cbuffer cbTreeConstanBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
    float4 eye; 
};

接下來就根據輸入的點信息來生成樹木模型。具體的數學原理在DirectX遊戲編程中有詳細的介紹,這裏主要關注其實現。

[maxvertexcount(4)]
void main(
    point GSInput input[1],
    inout TriangleStream<GSOutput > output
)
{
    //
    // 根據size計算樹木貼圖的四個頂點座標
    //
    float halfWidth =0.5f*input[0].size.x;
    float halfHeight =0.5f*input[0].size.y;
 
    float4 v[4];
    v[0] = float4(-halfWidth,-halfHeight, 0.0f, 1.0f);
    v[1] = float4(+halfWidth,-halfHeight, 0.0f, 1.0f);
    v[2] = float4(-halfWidth,+halfHeight, 0.0f, 1.0f);
    v[3] = float4(+halfWidth,+halfHeight, 0.0f, 1.0f);
 
    //
    // 四個頂點的紋理座標
    //
    float2 texC[4];
    texC[0] = float2(0.0f, 1.0f);
    texC[1] = float2(1.0f, 1.0f);
    texC[2] = float2(0.0f, 0.0f);
    texC[3] = float2(1.0f, 0.0f);
 
    //
    // 計算使貼圖面向觀察點的變換矩陣
    //
    float3 up = float3(0.0f, 1.0f, 0.0f);
    float3 look =input[0].center - eye.xyz;
    look.y =0.0f; 
    look =normalize(look);
    float3 right = cross(up,look);
 
    float4x4 W;
    W[0] = float4(right,           0.0f);
    W[1] = float4(up,              0.0f);
    W[2] = float4(look,            0.0f);
    W[3] = float4(input[0].center,1.0f);
 
    float4x4 gViewProj =mul(view, projection);
    float4x4 WVP =mul(W,gViewProj);
 
    //
    // 轉換頂點座標到世界空間
    // 輸出三角形帶
    //
    GSOutput gOut;
    [unroll]
    for(int i = 0; i < 4;++i)
    {
       gOut.posH    = mul(v[i], WVP);
       gOut.posW    = mul(v[i], W).xyz;
       gOut.normal  = look;
       gOut.tex     = texC[i];
 
       output.Append(gOut);
    }
}

有上一篇文章的基礎,着色器的代碼很容易理解。讀入一個頂點(即樹木貼圖的中心點座標和貼圖的尺寸),生成四個頂點,之後將四個頂點轉換到投影空間,並設置好其對應的紋理座標,接着就可以由像素着色器進行處理。從這個過程中可以看出,頂點能夠包含的內容是很廣泛的,並不僅僅是座標信息而已,感覺頂點應該是可由GPU處理的信息集合。

着色器編寫完成後,就能在程序中使用了。首先還是定義頂點和常量緩衝區的結構體,與着色器代碼對應。在Direct3Dbase.h中添加:

struct TreeVertex
{
    DirectX::XMFLOAT3 center;
    DirectX::XMFLOAT2 size;
};
 
struct TreeConstantBuffer
{
    DirectX::XMFLOAT4X4 model;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
    DirectX::XMFLOAT4 eye;
};

然後仿照之前的模型類編寫TreeModel類,負責生成樹木的頂點信息和渲染樹木貼圖。主要方法的代碼如下

void TreeModel::Initialize(ID3D11Device* d3dDevice)
{
    TreeVertex treeVertices[] =
    {
       {XMFLOAT3( 60.0,  GetHeight(60.0f, 50.0f), 50.0f), XMFLOAT2( 15.0f, 15.0f )},
       {XMFLOAT3( 20.0f,  GetHeight(20.0f, 30.0f), 30.0f), XMFLOAT2( 15.0f, 15.0f )},
       {XMFLOAT3( 20.0f,  GetHeight(20.0f, 40.0f), 40.0f), XMFLOAT2( 15.0f, 15.0f )},
       {XMFLOAT3( 50.0f,  GetHeight(50.0f, 10.0f), 10.0f), XMFLOAT2( 15.0f, 15.0f )},
    };
 
    m_indexCount = ARRAYSIZE(treeVertices);
 
    D3D11_SUBRESOURCE_DATA vertexBufferData ={0};
    vertexBufferData.pSysMem= treeVertices;
    vertexBufferData.SysMemPitch= 0;
    vertexBufferData.SysMemSlicePitch= 0;
 
    CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(treeVertices), D3D11_BIND_VERTEX_BUFFER);
    DX::ThrowIfFailed(
       d3dDevice->CreateBuffer(
       &vertexBufferDesc,
       &vertexBufferData,
       &m_vertexBuffer
       )
       );
}
 
void TreeModel::Render(ID3D11DeviceContext* d3dContext)
{
    UINT stride = sizeof(TreeVertex);
    UINT offset = 0;
 
    d3dContext->IASetVertexBuffers(
       0,
       1,
       m_vertexBuffer.GetAddressOf(),
       &stride,
       &offset
       );
 
    d3dContext->Draw(
       m_indexCount,
       0
       );
}
 
 
float TreeModel::GetHeight(float xPos, float zPos)
{
    return 8.0f + 0.3f * (zPos*sinf(0.1f*xPos) + xPos*cosf(0.1f*zPos));
}

注意,雖然實際繪製的是一個個樹木貼圖,但是從程序中看,繪製的只是一個個點,而不是兩個三角形拼成的矩形。所以,TreeModel中可以不用索引數組,同時要用Draw方法來渲染這個模型,而不是其他模型的DrawIndexed方法。另外,GetHeight方法在地標座標的基礎上增加了8.0f,保證樹木在地表上方。定義好樹木模型後,接下來要修改Renderer類,添加與樹木貼圖相關的成員:


    //----------------------------------------------------------
    // 樹木貼圖相關
    //----------------------------------------------------------
    void DrawTrees();
   
    TreeModel m_tree;
    Microsoft::WRL::ComPtr<ID3D11VertexShader>m_treeVertexShader;
    Microsoft::WRL::ComPtr<ID3D11GeometryShader>m_treeGeometryShader;
    Microsoft::WRL::ComPtr<ID3D11PixelShader>m_treePixelShader;
 
    Microsoft::WRL::ComPtr<ID3D11InputLayout> m_treeInputLayout;
    Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_treeSRV;
    Microsoft::WRL::ComPtr<ID3D11Buffer>m_treeConstantBuffer;
    TreeConstantBufferm_treeConstantBufferData;

然後在CreateDeviceResources方法中添加載入新着色器的代碼,並初始化樹木頂點的輸入佈局和常量緩衝區:


    auto loadTreeVSTask =DX::ReadDataAsync("TreeVertexShader.cso");
    auto loadTreeGSTask =DX::ReadDataAsync("TreeGeometryShader.cso");
    auto loadTreePSTask =DX::ReadDataAsync("TreePixelShader.cso");
 
    auto createTreeVSTask =loadTreeVSTask.then([this](Platform::Array<byte>^ fileData) {
       DX::ThrowIfFailed(
           m_d3dDevice->CreateVertexShader(
           fileData->Data,
           fileData->Length,
           nullptr,
           &m_treeVertexShader
           )
           );
 
       const D3D11_INPUT_ELEMENT_DESC treeVertexDesc[] =
       {
           { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,  0,  D3D11_INPUT_PER_VERTEX_DATA, 0 },
           { "SIZE",    0, DXGI_FORMAT_R32G32_FLOAT,    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
       };
 
       DX::ThrowIfFailed(
           m_d3dDevice->CreateInputLayout(
           treeVertexDesc,
           ARRAYSIZE(treeVertexDesc),
           fileData->Data,
           fileData->Length,
           &m_treeInputLayout
           )
           );
    });
 
    auto createTreeGSTask =loadTreeGSTask.then([this](Platform::Array<byte>^ fileData) {
       DX::ThrowIfFailed(
           m_d3dDevice->CreateGeometryShader(
           fileData->Data,
           fileData->Length,
           nullptr,
           &m_treeGeometryShader
           )
           );
 
      CD3D11_BUFFER_DESCtreeConstantBufferDesc(sizeof(TreeConstantBuffer), D3D11_BIND_CONSTANT_BUFFER);
       DX::ThrowIfFailed(
           m_d3dDevice->CreateBuffer(
           &treeConstantBufferDesc,
           nullptr,
           &m_treeConstantBuffer
           )
           );
    });
 
    auto createTreePSTask =loadTreePSTask.then([this](Platform::Array<byte>^ fileData) {
       DX::ThrowIfFailed(
           m_d3dDevice->CreatePixelShader(
           fileData->Data,
           fileData->Length,
           nullptr,
           &m_treePixelShader
           )
           );
    });

之後還要初始化各個模型:


    auto createModelTask =(createPSTask && createVSTask).then([this] () {
       m_hill.Initialize(m_d3dDevice.Get(),128, 128);
       m_water.Initialize(m_d3dDevice.Get(),128, 128, 1.0f, 0.03f, 3.25f, 0.4f);
       m_cube.Initialize(m_d3dDevice.Get(),XMFLOAT2(60.0f, 30.0f));
       m_tree.Initialize(m_d3dDevice.Get());
    });

不要忘了還要添加載入樹木紋理的代碼(這裏的紋理使用的是DirectX遊戲編程入門的資源)。


DX::ThrowIfFailed(
       CreateDDSTextureFromFile(
       m_d3dDevice.Get(),
       L"Texture/tree0.dds",
       NULL,
       m_treeSRV.GetAddressOf()
       )
       );

還有最後一項需要填充的內容,常量緩衝區。樹木模型與其他模型都在一個空間內,所以常量也相同,只是組織結構不同。爲了方便幾何着色器使用這些常量,所以新定義一個常量緩衝區。填充這個緩衝區在Update方法中進行:


// 更新樹木模型緩衝區
m_treeConstantBufferData.model= m_constantBufferData.model;
m_treeConstantBufferData.view= m_constantBufferData.view;
m_treeConstantBufferData.projection= m_constantBufferData.projection;
XMStoreFloat4(&m_treeConstantBufferData.eye,eye);

完成後就能進入到渲染流程。下面的DrawTrees方法是仿照已有的繪製模型過程編寫的。由於繪製樹木模型用到的資源與繪製其它模型完全不同,所以爲樹木模型渲染單獨創建這個方法。

void Renderer::DrawTrees()
{
    // 設置圖元類型爲點並修改輸入佈局
    m_d3dContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
    m_d3dContext->IASetInputLayout(m_treeInputLayout.Get());
 
    // 設置頂點着色器
    m_d3dContext->VSSetShader(
       m_treeVertexShader.Get(),
       nullptr,
       0
       );
 
    // 設置幾何着色器及其常量緩衝區
    m_d3dContext->GSSetShader(
       m_treeGeometryShader.Get(),
       nullptr,
       0
       );
 
    m_d3dContext->GSSetConstantBuffers(
       0,               
       1,               
       m_treeConstantBuffer.GetAddressOf()
       );
 
    m_d3dContext->UpdateSubresource(
       m_treeConstantBuffer.Get(),
       0,
       NULL,
       &m_treeConstantBufferData,
       0,
       0
       );
 
    // 設置像素着色器
    m_d3dContext->PSSetShader(
       m_treePixelShader.Get(),
       nullptr,
       0
       );
 
    // 設置樹木紋理
    m_d3dContext->PSSetShaderResources(
       0,
       1,
       m_treeSRV.GetAddressOf()
       );
 
    // 設置紋理採樣器
    m_d3dContext->PSSetSamplers(
       0,
       1,
       m_Sampler.GetAddressOf()
       );
 
    // 設置渲染模式
    SetFillMode(D3D11_FILL_SOLID);
 
    m_tree.Render(m_d3dContext.Get());
}

因爲現在繪製的是點圖元,所以第一句IASetPrimitiveTopology方法的參數是D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,之前都是繪製三角形,用的參數是D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST

完成DrawTrees方法後就可以在Render方法中調用它來實現樹木的繪製。這裏還有一點要注意,因爲繪製普通模型無需幾何着色器,所以在繪製過程中要添加關閉幾何着色器的代碼,不然在渲染普通模型時也會使用幾何着色器,而幾何着色器的輸出與普通的像素着色器輸入並不對應,會使渲染結果出錯。


// 關閉幾何着色器
m_d3dContext->GSSetShader(
    NULL,
    nullptr,
    0
    );

實際運行效果如下圖:



本篇文章源代碼:Direct3DApp_HillWaveTree

原文地址:http://blog.csdn.net/raymondcode/article/details/8528159

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章