纹理贴图映射(texturemapping)是可以显著提高场景细节和真实感的一种技术,基本原理是将图像数据映射到3D三角形表面(之前的文章提到过,三维模型其实是由很多个三角形拼接而成)。当使用纹理资源时,只要将每个3D三角形与纹理资源上的三角形对应,就可以实现贴图效果。如图1,有一个立方体模型和纹理贴图,将立方体上的点与纹理贴图上的点对应,就像给一个没有颜色的正方体贴一层木纹包装纸。
Direct3D的纹理坐标系由表示图像水平方向的u轴和表示图像垂直方向的v轴组成。坐标 (u,v) 指定了纹理上的一个元素,该元素称为纹理元素(texture element),其中 0≤u,v≤1。将规范化坐标区间设为[0,1]是因为这样可以使Direct3D拥有一个独立于纹理尺寸的坐标空间。即无论纹理的实际尺寸是 256×256还是512×512,(0.5,0.5)永远表示中间的纹理元素。
另外还有一个问题,纹理资源不管多精细都是由离散的数据点组成,如果指定的纹理坐标(u,v)与任何一个纹理元素点都不对应时该怎么办?如图1中的立方体。假设木头纹理的分辨率为 512×512,显示器的分辨率为 1024×1024,当观察点逐渐靠近立方体,立方体会被放大,甚至能盖住整个屏幕。这时就需要用很少的纹理元素来覆盖很多的像素,称为倍增。与倍增相反的问题是缩减,要用很多的纹理元素来覆盖很少的像素。DirectX为解决这些问题定义了多种过滤器,如点过滤和线性过滤。使用时只要对应好顶点和其纹理坐标,过滤器就能通过插值或抽取估计顶点之间每个像素的颜色。
下面就来实现纹理贴图映射。
使用模版新建Direct3D立方体项目。首先依然是更改HLSL代码。
顶点着色器部分:使用纹理资源时不需要指定颜色,所以用这部分空间存储顶点的法向量,用于计算光照效果。添加纹理坐标成员,它与3D顶点坐标对应。这样,每3个顶点构成的3D三角形在纹理空间中都会有一个对应的2D纹理三角形。代码如下:
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 normal : NORMAL;
float2 tex : TEXCOOD;
};
struct VertexShaderOutput
{
float4 pos : SV_POSITION;
float3 normal : NORMAL;
float2 tex : TEXCOOD;
};
VertexShaderOutput main(VertexShaderInput input)
{
VertexShaderOutputoutput;
float4 pos = float4(input.pos, 1.0f);
// 转换坐标到投影空间
pos = mul(pos,model);
pos = mul(pos,view);
pos = mul(pos,projection);
output.pos =pos;
// 转换法向量到世界空间用于光照计算
float4 normal = float4(normalize(input.normal),0.0f);
normal =mul(normal, model);
output.normal =normalize(normal.xyz);
// 纹理坐标不需要改动
output.tex =input.tex;
return output;
}
像素着色器部分:输入结构体定义必须和顶点着色器的输出结构体格式一致。还要添加纹理资源,并在main方法中添加简单漫反射光的计算。另外,使用过滤器访问纹理资源需要通过采样器。代码如下:
SamplerState samplerLinear : register(s0);
Texture2D woodDiffuse : register(t0);
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float3 normal : NORMAL;
float2 tex : TEXCOOD;
};
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 lightDirection =normalize(float3(1, -1, 0));
float4 texelColor = woodDiffuse.Sample(samplerLinear,input.tex);
// 计算简单漫反射
float lightMagnitude =0.8f * saturate(dot(input.normal, -lightDirection)) + 0.2f;
return texelColor *lightMagnitude;
}
HLSL代码完成后就要修改主程序。在Windows 8 Store App中载入纹理资源可以使用WICTextureLoader。它支持读取多种图片资源(jpg、png)创建纹理。使用时将.cpp和.h文件加入项目即可。如果想载入DDS格式的资源可以看DirectXTex的说明。
完成后在CubeRenderer类里更改结构体定义,与着色器对应:
struct VertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
DirectX::XMFLOAT2 tex;
};
然后添加三个成员以使用纹理资源和采样器:
ID3D11Resource* tex;
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> m_WoodSRV;
Microsoft::WRL::ComPtr<ID3D11SamplerState> m_Sampler;
接着就开始修改初始化部分。在CreateDeviceResources方法中在载入顶点着色器和像素着色器之后添加createWoodTexTask,用于初始化纹理资源和采样器:
auto createWoodTexTask = (createPSTask &&createVSTask).then([this] () {
DX::ThrowIfFailed(
CreateWICTextureFromFile(
m_d3dDevice.Get(),
m_d3dContext.Get(),
L"wood.jpg",
&tex,
m_WoodSRV.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.BorderColor[0 ] = 1.0f;
samplerDesc.BorderColor[1 ] = 1.0f;
samplerDesc.BorderColor[2 ] = 1.0f;
samplerDesc.BorderColor[3 ] = 1.0f;
samplerDesc.MinLOD= -3.402823466e+38F; // -FLT_MAX
samplerDesc.MaxLOD= 3.402823466e+38F; // FLT_MAX
DX::ThrowIfFailed(
m_d3dDevice->CreateSamplerState(
&samplerDesc,
m_Sampler.GetAddressOf()
)
);
});
接下来自然是设置顶点缓冲区和索引数组。顶点的数据需要根据结构体的变化修改:
auto createCubeTask = createWoodTexTask.then([this] () {
VertexPositionColor cubeVertices[] =
{
// +x
{XMFLOAT3( 0.5f, 0.5f, -0.5f), XMFLOAT3( 1.0f, 0.0f, 0.0f),XMFLOAT2(0.0f, 0.0f), },
{XMFLOAT3( 0.5f, 0.5f, 0.5f), XMFLOAT3( 1.0f, 0.0f, 0.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3( 0.5f, -0.5f, 0.5f), XMFLOAT3( 1.0f, 0.0f, 0.0f),XMFLOAT2(1.0f, 1.0f), },
{XMFLOAT3( 0.5f, -0.5f,-0.5f), XMFLOAT3( 1.0f, 0.0f, 0.0f), XMFLOAT2(0.0f, 1.0f), },
// -x
{XMFLOAT3(-0.5f, 0.5f, -0.5f), XMFLOAT3(-1.0f, 0.0f, 0.0f),XMFLOAT2(0.0f, 0.0f), },
{XMFLOAT3(-0.5f, 0.5f, 0.5f), XMFLOAT3(-1.0f, 0.0f, 0.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3(-1.0f, 0.0f, 0.0f),XMFLOAT2(1.0f, 1.0f), },
{XMFLOAT3(-0.5f, -0.5f,-0.5f), XMFLOAT3(-1.0f, 0.0f, 0.0f), XMFLOAT2(0.0f, 1.0f), },
// +y
{XMFLOAT3(-0.5f, 0.5f, 0.5f), XMFLOAT3( 0.0f, 1.0f, 0.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3( 0.5f, 0.5f, 0.5f), XMFLOAT3( 0.0f, 1.0f, 0.0f), XMFLOAT2(0.0f, 1.0f), },
{XMFLOAT3( 0.5f, 0.5f, -0.5f), XMFLOAT3( 0.0f, 1.0f, 0.0f),XMFLOAT2(1.0f, 1.0f), },
{XMFLOAT3(-0.5f, 0.5f, -0.5f), XMFLOAT3( 0.0f, 1.0f, 0.0f),XMFLOAT2(0.0f, 0.0f), },
// -y
{XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3( 0.0f, -1.0f,0.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3( 0.5f, -0.5f, 0.5f), XMFLOAT3( 0.0f, -1.0f,0.0f), XMFLOAT2(0.0f, 1.0f), },
{XMFLOAT3( 0.5f, -0.5f,-0.5f), XMFLOAT3( 0.0f, -1.0f, 0.0f), XMFLOAT2(1.0f, 1.0f), },
{XMFLOAT3(-0.5f, -0.5f,-0.5f), XMFLOAT3( 0.0f, -1.0f, 0.0f), XMFLOAT2(0.0f, 0.0f), },
// +z
{XMFLOAT3(-0.5f, 0.5f, 0.5f), XMFLOAT3( 0.0f, 0.0f, 1.0f), XMFLOAT2(0.0f, 0.0f), },
{XMFLOAT3( 0.5f, 0.5f, 0.5f), XMFLOAT3( 0.0f, 0.0f, 1.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3( 0.5f, -0.5f, 0.5f), XMFLOAT3( 0.0f, 0.0f, 1.0f),XMFLOAT2(1.0f, 1.0f), },
{XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT3( 0.0f, 0.0f, 1.0f),XMFLOAT2(0.0f, 1.0f), },
// -z
{XMFLOAT3(-0.5f, 0.5f, -0.5f), XMFLOAT3( 0.0f, 0.0f,-1.0f), XMFLOAT2(1.0f, 0.0f), },
{XMFLOAT3( 0.5f, 0.5f, -0.5f), XMFLOAT3( 0.0f, 0.0f,-1.0f), XMFLOAT2(0.0f, 0.0f), },
{XMFLOAT3( 0.5f, -0.5f,-0.5f), XMFLOAT3( 0.0f, 0.0f, -1.0f), XMFLOAT2(0.0f, 1.0f), },
{XMFLOAT3(-0.5f, -0.5f,-0.5f), XMFLOAT3( 0.0f, 0.0f, -1.0f), XMFLOAT2(1.0f, 1.0f), },
};
D3D11_SUBRESOURCE_DATA vertexBufferData ={0};
vertexBufferData.pSysMem= cubeVertices;
vertexBufferData.SysMemPitch= 0;
vertexBufferData.SysMemSlicePitch= 0;
CD3D11_BUFFER_DESC vertexBufferDesc(sizeof(cubeVertices), D3D11_BIND_VERTEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&m_vertexBuffer
)
);
unsigned short cubeIndices[] =
{
0,2,1, // +x
0,3,2,
4,5,6, // -x
4,6,7,
8,10,9, // +y
8,11,10,
12,13,14,// -y
12,14,15,
16,17,18,// +z
16,18,19,
20,22,21,// -z
20,23,22,
};
m_indexCount= ARRAYSIZE(cubeIndices);
D3D11_SUBRESOURCE_DATA indexBufferData ={0};
indexBufferData.pSysMem= cubeIndices;
indexBufferData.SysMemPitch= 0;
indexBufferData.SysMemSlicePitch= 0;
CD3D11_BUFFER_DESC indexBufferDesc(sizeof(cubeIndices), D3D11_BIND_INDEX_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&indexBufferDesc,
&indexBufferData,
&m_indexBuffer
)
);
});
完成后就可以在渲染时使用纹理资源了。在Render方法里添加以下代码使纹理资源生效:
// 设置纹理资源
m_d3dContext->PSSetShaderResources(
0,
1,
m_WoodSRV.GetAddressOf()
);
// 设置纹理采样器
m_d3dContext->PSSetSamplers(
0,
1,
m_Sampler.GetAddressOf()
);
运行后效果如下图:
本篇文章的源代码: