要使用DirectX來獲得三維效果,一般首先要生成一個三維模型,然後計算它在可視空間中的投影。這樣得到的二維圖像十分真實,但是計算量也很大。在大規模場景渲染中,隨着模型精度的提高,這樣的處理方式十分消耗資源。人眼的分辨率是有限的,對於遠處的模型,模糊一些不會影響到整體效果。Billboard技術就是用二維圖片來模擬三維模型的投影,從而提高渲染效率。只要距離足夠遠,通過將二維圖片旋轉至合適角度,實際渲染效果與三維模型相差無幾,但計算量減少很多。本文使用幾何着色器,利用Billboard技術在之前的模型中添加樹木貼圖。
整個過程與上一篇的內容類似。不過這一次樹木模型的頂點結構與其他模型不同,所以要重新寫一套着色器(TreeVertexShader.hlsl、TreeGeometryShader.hlsl、TreePixelShader.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