Windows 8 Directx 開發學習筆記(十一)地形紋理貼圖
前一篇實現木箱貼圖時,木箱的六個面都正好用一整張紋理圖,即六個面的紋理座標均在[0,1]內。然而在為比較大的模型貼圖時,像山峰河谷模型,如果只用一張紋理圖,那麼每個三角形只得到幾個紋理元素,無法為提供足夠高的解析度。這時可以在模型表面上平鋪紋理貼圖,像給牆面貼磁磚一樣,只需要知道一個單位的貼圖,就能鋪滿整個表面,從而獲得較高的解析度。實現起來其實很簡單,只要變換紋理座標的範圍,同時將紋理定址模式設定為重複即可。
紋理座標範圍變換有兩種方式,很簡單,如圖1:
其中x,y是原始紋理座標,為了能與4*4的變換矩陣相乘,將其擴充套件為4元向量。等號左邊是一種變換方式,使用變換矩陣進行紋理座標變換,可用GPU計算。等號右邊是第二種變換方式,直接生成變換後的紋理座標,由CPU計算。本文選擇的是第一種變換方式。
下面就開始給學習筆記(九)實現的山峰和水面模型貼圖。
依然是先修改HLSL程式碼。在頂點著色器的輸入輸出結構中新增用於儲存紋理座標的成員tex。並在ModelViewProjectionConstantBuffer裡新增texTransform成員,用於儲存縮放和平移變換矩陣。
struct VertexShaderInput { float3 pos : POSITION; float3 normal : NORMAL; float2 tex : TEXCOOD; }; struct VertexShaderOutput { float4 posH : SV_POSITION; float3 posW : POSITION; float3 normal : NORMAL; float2 tex : TEXCOOD; }; cbuffer ModelViewProjectionConstantBuffer : register(b0) { matrix model; matrix view; matrix projection; matrix texTransform; };
變換紋理座標時只要在main方法中新增下面的語句即可:
// 紋理座標變換
output.tex = mul(float4(input.tex, 0.0f, 1.0f),texTransform).xy;
在畫素著色器中要定義取樣器和紋理資源,並更改輸入結構體,與頂點著色器的輸出對應。
SamplerState samplerLinear : register(s0); Texture2D texDiffuse : register(t0); struct PixelShaderInput { float4 posH : SV_POSITION; float3 posW : POSITION; float3 normal : NORMAL; float2 tex : TEXCOOD; };
為了減少更改,在main方法中還是使用Material結構進行光照計算,只是Diffuse成員由紋理取樣獲得。
Material textureMat = gMaterial;
textureMat.Diffuse = texDiffuse.Sample(samplerLinear,input.tex);
HLSL程式碼修改至此完成,接下來就要修改C++程式碼。
結構先行,修改Directl3DBase.h中的頂點結構體和常量緩衝區定義,保證它們的結構和頂點著色器中定義的一致。
struct VertexPosition
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT2 tex;
};
struct ModelViewProjectionConstantBuffer
{
DirectX::XMFLOAT4X4 model;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
DirectX::XMFLOAT4X4 gTexTransform;
};
由於頂點結構體改變,HillModel和WaterModel裡頂點初始化程式碼需要修改。HillModel的Initialize方法裡,頂點初始化程式碼修改如下:
const float dx = 1.0f;
const float du = 1.0f/(xRange-1);
const float dv = 1.0f/(zRange-1);
VertexPosition *Vertices = new VertexPosition[xRange*zRange];
for(int row=0; row<zRange; ++row)
{
float zPos = row*dx;
for(int col=0;col<xRange; ++col)
{
float xPos = col*dx;
float yPos = 0.3f *(zPos*sinf(0.1f*xPos) + xPos*cosf(0.1f*zPos));
Vertices[xRange*row+ col].pos = XMFLOAT3(xPos, yPos, zPos);
Vertices[xRange*row+ col].normal = GetHillNormal(xPos, zPos);
Vertices[xRange*row+ col].tex.x = row*du;
Vertices[xRange*row+ col].tex.y = col*dv;
}
}
du和dv可保證生成的紋理座標正好在[0,1]範圍內,目的是方便理解縮放比例,並不是必須的。WaterModel生成紋理座標的過程與上面類似,是在Update方法中進行的。
// 更新頂點緩衝區
const float du = 1.0f/xRange;
const float dv = 1.0f/zRange;
for(uint32 i = 0; i < m_vertexCount; ++i)
{
vertex[i].pos =mCurrSolution[i];
vertex[i].tex.x= (i/xRange)*du;
vertex[i].tex.y= (i%zRange)*dv;
vertex[i].normal= mNormal[i];
}
這裡雖然每次更新都要重新計算一樣的紋理座標,效率很低,不過鑑於規模小,方便理解就直接修改了。
完成以上工作後就可以開始載入紋理。與木箱貼圖一樣,向工程中新增WICTextureLoader的標頭檔案和原始檔。然後在Renderer類裡新增以下成員,儲存兩種紋理資源,分別代表陸地和水面。
ID3D11Resource* tex;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_LandSRV;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_WaterSRV;
Microsoft::WRL::ComPtr<ID3D11SamplerState> m_Sampler;
然後在CreateDeviceResources方法中載入並初始化紋理資源,初始化取樣器,當然還要修改輸入佈局:
const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOOD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
……
auto createTexTask = (createPSTask &&createVSTask).then([this] () {
// 載入紋理資源
DX::ThrowIfFailed(
CreateWICTextureFromFile(
m_d3dDevice.Get(),
m_d3dContext.Get(),
L"Texture/grass.jpg",
&tex,
m_LandSRV.GetAddressOf()
)
);
DX::ThrowIfFailed(
CreateWICTextureFromFile(
m_d3dDevice.Get(),
m_d3dContext.Get(),
L"Texture/water.jpg",
&tex,
m_WaterSRV.GetAddressOf()
)
);
// 初始化取樣器
D3D11_SAMPLER_DESC samplerDesc;
samplerDesc.Filter= D3D11_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW= D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.MipLODBias= 0;
samplerDesc.MaxAnisotropy= 1;
samplerDesc.ComparisonFunc= D3D11_COMPARISON_NEVER;
samplerDesc.MinLOD= -3.402823466e+38F; // -FLT_MAX
samplerDesc.MaxLOD= 3.402823466e+38F; // FLT_MAX
DX::ThrowIfFailed(
m_d3dDevice->CreateSamplerState(
&samplerDesc,
m_Sampler.GetAddressOf()
)
);
});
初始化完成後,就能在Render方法設定並使用紋理資源。
// 設定紋理取樣器
m_d3dContext->PSSetSamplers(
0,
1,
m_Sampler.GetAddressOf()
);
// 設定陸地紋理
m_d3dContext->PSSetShaderResources(
0,
1,
m_LandSRV.GetAddressOf()
);
// 設定陸地紋理縮放
XMMATRIX landScale = XMMatrixScaling(5.0f, 5.0f, 0.0f);
XMStoreFloat4x4(&m_constantBufferData.gTexTransform,XMMatrixTranspose(landScale));
// 設定陸地材質
m_constantLightBufferData.gMaterial= m_landMat;
……
// 設定水面紋理
m_d3dContext->PSSetShaderResources(
0,
1,
m_WaterSRV.GetAddressOf()
);
// 設定水面紋理縮放
XMMATRIX waterScale = XMMatrixScaling(5.0f, 5.0f, 0.0f);
XMStoreFloat4x4(&m_constantBufferData.gTexTransform,XMMatrixTranspose(waterScale));
// 設定水面材質
m_constantLightBufferData.gMaterial = m_wavesMat;
DirectX裡包含多個生成變換矩陣的方法,如上面的XMMatrixScaling方法生成縮放變換矩陣,後面將用到的XMMatrixTranslation方法可以生成平移變換矩陣。另外注意每次設定好紋理材質後,不但要呼叫UpdateSubresource方法更新m_constantLightBuffer,還要用它更新m_constantBuffer,因為紋理縮放矩陣是ModelViewProjectionConstantBuffer的成員。
圖1載入的是grass紋理:
圖2載入的是rock紋理:
以上兩個模型中,水面紋理是不變的,而且沒有流動的效果。簡單的流動效果可以通過紋理座標隨時間偏移實現。
在Renderer類裡新增偏移量成員:
XMFLOAT2 m_waterTexOffset;
在Update方法裡新增程式碼,讓偏移量隨時間變化。
// 更新紋理偏移量
m_waterTexOffset.y += 0.1f*timeDelta;
m_waterTexOffset.x += 0.1f*timeDelta;
然後在Render方法中使用偏移量生成平移變換矩陣,並與縮放變換矩陣相乘,這樣水面紋理座標就會保持平鋪效果,並隨時間平移,實現類似流動的效果:
// 設定水面紋理縮放和偏移量
XMMATRIX waterScale = XMMatrixScaling(5.0f, 3.0f, 0.0f);
XMMATRIX waterOffset = XMMatrixTranslation(m_waterTexOffset.x,m_waterTexOffset.y, 0.0f);
XMStoreFloat4x4(&m_constantBufferData.gTexTransform,XMMatrixTranspose(waterScale*waterOffset));
本篇文章的原始碼