要使用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