剖析板条箱演示程序
板条箱演示程序:
本程序是在 立方体演示程序 的基础上做改动。主要改动为:
- 增加了两个光源
- 给立方体每个面贴上一层纹理
注意:理解本程序内容需要有 Games-101-Texture Mapping 及其系列文章的基础。
定义光源和材质
首先是对光源和材质进行初始化:
CrateApp::CrateApp(HINSTANCE hInstance)
: D3DApp(hInstance), mBoxVB(0), mBoxIB(0), mDiffuseMapSRV(0),
mEyePosW(0.0f, 0.0f, 0.0f), mTheta(1.3f * MathHelper::Pi),
mPhi(0.4f * MathHelper::Pi), mRadius(2.5f) {
mMainWndCaption = L"Crate Demo";
mLastMousePos.x = 0;
mLastMousePos.y = 0;
XMMATRIX I = XMMatrixIdentity();
XMStoreFloat4x4(&mBoxWorld, I);
XMStoreFloat4x4(&mTexTransform, I);
XMStoreFloat4x4(&mView, I);
XMStoreFloat4x4(&mProj, I);
mDirLights[0].Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
mDirLights[0].Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
mDirLights[0].Specular = XMFLOAT4(0.6f, 0.6f, 0.6f, 16.0f);
mDirLights[0].Direction = XMFLOAT3(0.707f, -0.707f, 0.0f);
mDirLights[1].Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
mDirLights[1].Diffuse = XMFLOAT4(1.4f, 1.4f, 1.4f, 1.0f);
mDirLights[1].Specular = XMFLOAT4(0.3f, 0.3f, 0.3f, 16.0f);
mDirLights[1].Direction = XMFLOAT3(-0.707f, 0.0f, 0.707f);
mBoxMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
mBoxMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
mBoxMat.Specular = XMFLOAT4(0.6f, 0.6f, 0.6f, 16.0f);
}
由前几章的知识我们知道,第一个平行光以 45° 角打在顶面与左面;第二个平行光跟对角线 平行且由 指向 (也就是主要受光面是正面和右面):
由演示中我们也可以看出,底面和背面由于没有接收到直接光照,因此是暗淡的(但是因为有环境光,所以没有全黑)。
在 DirectX 中,纹理坐标系的 轴正方向是向下的,跟 Games-101 中的描述相反,但是不影响理解。
初始化
在 CrateApp 初始化的过程中主要做了两个事情:
- 从文件中读取纹理数据
- 创建模型及其顶点属性
bool CrateApp::Init() {
if (!D3DApp::Init())
return false;
// Must init Effects first since InputLayouts depend on shader signatures.
Effects::InitAll(md3dDevice);
InputLayouts::InitAll(md3dDevice);
HR(D3DX11CreateShaderResourceViewFromFile(
md3dDevice, L"Textures/WoodCrate01.dds", 0, 0, &mDiffuseMapSRV, 0));
BuildGeometryBuffers();
return true;
}
函数 D3DX11CreateShaderResourceViewFromFile() 的调用做了两个事情:
- 从文件中读取图像数据(支持 BMP、JPG、PNG、DDS、TIFF、GIF、WMP(参见 D3DX11_IMAGE_FILE_FORMAT))
- 为纹理创建对应的着色器资源视图(纹理资源需要通过视图才能绑定到渲染管线上)
上述两个过程也可以通过分别调用 D3DX11CreateTextureFromFile() 和 CreateShaderResourceView() 来完成。
DDS(DirectDraw Surface)文件是一种常用的纹理文件格式,它可以直接存储压缩纹理。纹理需要驻留在 GPU 中才能被着色器快速访问,对纹理进行压缩可以缓解 GPU 的内存压力,在使用时再由 GPU 实时解压缩。另外压缩纹理还能减少对磁盘空间的占用量。更详细的信息见:压缩纹理格式。
因为增加了纹理的需求,在这个程序里面顶点的结构也被修改为如下所示:
namespace Vertex {
// Basic 32-byte vertex structure.
struct Basic32 {
XMFLOAT3 Pos;
XMFLOAT3 Normal;
XMFLOAT2 Tex; // 纹理坐标
};
} // namespace Vertex
在构造立方体时,也不再直接指定顶点数据,而是通过 山峰演示程序 中描述的工具类 GeometryGenerator 通过其 CreateBox() 接口进行创建:
void CrateApp::BuildGeometryBuffers() {
GeometryGenerator::MeshData box;
GeometryGenerator geoGen;
geoGen.CreateBox(1.0f, 1.0f, 1.0f, box);
// Cache the vertex offsets to each object in the concatenated vertex buffer.
mBoxVertexOffset = 0;
// Cache the index count of each object.
mBoxIndexCount = box.Indices.size();
// Cache the starting index for each object in the concatenated index buffer.
mBoxIndexOffset = 0;
UINT totalVertexCount = box.Vertices.size();
UINT totalIndexCount = mBoxIndexCount;
//
// Extract the vertex elements we are interested in and pack the
// vertices of all the meshes into one vertex buffer.
//
std::vector<Vertex::Basic32> vertices(totalVertexCount);
UINT k = 0;
for (size_t i = 0; i < box.Vertices.size(); ++i, ++k) {
vertices[k].Pos = box.Vertices[i].Position;
vertices[k].Normal = box.Vertices[i].Normal;
vertices[k].Tex = box.Vertices[i].TexC;
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex::Basic32) * totalVertexCount;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &vertices[0];
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mBoxVB));
//
// Pack the indices of all the meshes into one index buffer.
//
std::vector<UINT> indices;
indices.insert(indices.end(), box.Indices.begin(), box.Indices.end());
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * totalIndexCount;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &indices[0];
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mBoxIB));
}
功能函数 CreateBox() 能够创建指定大小的长方体模型,并计算好顶点法线、切线向量、纹理坐标等值:
// GeometryGenerator 内部的顶点结构
struct Vertex {
Vertex() {}
Vertex(const XMFLOAT3 &p, const XMFLOAT3 &n, const XMFLOAT3 &t,
const XMFLOAT2 &uv)
: Position(p), Normal(n), TangentU(t), TexC(uv) {}
Vertex(float px, float py, float pz, float nx, float ny, float nz, float tx,
float ty, float tz, float u, float v)
: Position(px, py, pz), Normal(nx, ny, nz), TangentU(tx, ty, tz),
TexC(u, v) {}
XMFLOAT3 Position;
XMFLOAT3 Normal;
XMFLOAT3 TangentU;
XMFLOAT2 TexC;
};
void GeometryGenerator::CreateBox(float width, float height, float depth,
MeshData &meshData) {
//
// Create the vertices.
//
Vertex v[24];
// 缩小一半
float w2 = 0.5f * width;
float h2 = 0.5f * height;
float d2 = 0.5f * depth;
// Fill in the front face vertex data.
v[0] = Vertex(-w2, -h2, -d2, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
v[1] = Vertex(-w2, +h2, -d2, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
v[2] = Vertex(+w2, +h2, -d2, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f);
v[3] = Vertex(+w2, -h2, -d2, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
// Fill in the back face vertex data.
v[4] = Vertex(-w2, -h2, +d2, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
v[5] = Vertex(+w2, -h2, +d2, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
v[6] = Vertex(+w2, +h2, +d2, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
v[7] = Vertex(-w2, +h2, +d2, 0.0f, 0.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f);
// Fill in the top face vertex data.
v[8] = Vertex(-w2, +h2, -d2, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
v[9] = Vertex(-w2, +h2, +d2, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
v[10] = Vertex(+w2, +h2, +d2, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f);
v[11] = Vertex(+w2, +h2, -d2, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
// Fill in the bottom face vertex data.
v[12] =
Vertex(-w2, -h2, -d2, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f);
v[13] =
Vertex(+w2, -h2, -d2, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f);
v[14] =
Vertex(+w2, -h2, +d2, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
v[15] =
Vertex(-w2, -h2, +d2, 0.0f, -1.0f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f);
// Fill in the left face vertex data.
v[16] =
Vertex(-w2, -h2, +d2, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f);
v[17] =
Vertex(-w2, +h2, +d2, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
v[18] =
Vertex(-w2, +h2, -d2, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f);
v[19] =
Vertex(-w2, -h2, -d2, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f);
// Fill in the right face vertex data.
v[20] = Vertex(+w2, -h2, -d2, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f);
v[21] = Vertex(+w2, +h2, -d2, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f);
v[22] = Vertex(+w2, +h2, +d2, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f);
v[23] = Vertex(+w2, -h2, +d2, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f);
meshData.Vertices.assign(&v[0], &v[24]);
// 顶点索引
...
}
每 4 个顶点一组代表的是立方体的一个面,每个顶点最后两个参数代表的是纹理坐标(uv 坐标)。参照最前面的演示程序以及图二可以看到它们是匹配的。
仔细观察我们会注意到顶点坐标有重复。比如前面和上面交界的顶点 1 和 8,以及 2 和 11:
为什么会有这个重复呢?这是因为同一个顶点(比如 1 和 8)在不同的面上(比如前面和上面)需要表现出不同的纹理坐标(如果是前面,它表示纹理的左上角 (0, 0),如果是上面,它表示纹理的左下角 (0, 1)),以此来控制纹理的位置。其他重复的顶点坐标也是同样的原因。
纹理采样
我们看一下本程序关于 Effect 文件的主要改动:
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
SamplerState samAnisotropic {
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
AddressU = WRAP;
AddressV = WRAP;
};
struct VertexIn {
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut {
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD;
};
在结构上,增加了 gDiffuseMap 变量用于跟 C++ 中的纹理资源做交互,使得着色器中的代码可以访问纹理资源。另外我们还给 VertexIn 和 VertexOut 结构增加了纹理坐标字段供着色使用。
顶点着色器并没有太大的改动,只是增加了纹理坐标的计算:
VertexOut VS(VertexIn vin) {
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Output vertex attributes for interpolation across triangle.
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
变量 gTexTransform 表示要对纹理进行何种变换(平移、旋转、缩放等,在 剖析地形纹理演示程序 中有对其中的缩放变换做介绍),在这个程序里它是个单位矩阵,表示不做任何变换。而像素着色器则修改成如下所示:
float4 PS(VertexOut pin, uniform int gLightCount, uniform bool gUseTexure)
: SV_Target {
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// The toEye vector is used in lighting.
float3 toEye = gEyePosW - pin.PosW;
// Cache the distance to the eye from this surface point.
float distToEye = length(toEye);
// Normalize.
toEye /= distToEye;
// Default to multiplicative identity.
float4 texColor = float4(1, 1, 1, 1);
if (gUseTexure) {
// Sample texture.
texColor = gDiffuseMap.Sample(samAnisotropic, pin.Tex);
}
//
// Lighting.
//
float4 litColor = texColor;
if (gLightCount > 0) {
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// Sum the light contribution from each light source.
[unroll] for (int i = 0; i < gLightCount; ++i) {
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLights[i], pin.NormalW, toEye, A,
D, S);
ambient += A;
diffuse += D;
spec += S;
}
// Modulate with late add.
litColor = texColor * (ambient + diffuse) + spec;
}
// Common to take alpha from diffuse material and texture.
litColor.a = gMaterial.Diffuse.a * texColor.a;
return litColor;
}
像素着色器函数增加了一个参数用以控制是否使用纹理。我们通过函数中的以下代码来提取(采样)指定坐标的纹理颜色:
if (gUseTexure) {
// Sample texture.
texColor = gDiffuseMap.Sample(samAnisotropic, pin.Tex);
}
其中 samAnisotropic 是一种跟纹理相关的 SamplerState(采样器)对象。它的作用其实是对应 Games-101-Texture Queries 里面描述的问题(纹理过大或过小)的解决方案。在本程序中,它被定义成如下结构:
SamplerState samAnisotropic {
Filter = ANISOTROPIC; // 各向异性过滤
MaxAnisotropy = 4; // 各向异性的最大值 1~16
AddressU = WRAP; // 将U轴(水平)纹理坐标的寻址模式设置为"WRAP",意味着在采样超出纹理边界时,纹理将环绕。
AddressV = WRAP; // 将V轴(垂直)纹理坐标的寻址模式设置为"WRAP",与前一行类似。
};
字段 AddressU 和 AddressV 指定寻址模式,具体见:寻址模式。
在根据光源数量计算出光照的影响后,我们把纹理颜色作为材质的属性并将其整合到最终的颜色里面(不需要包括高光):
// Modulate with late add.
litColor = texColor * (ambient + diffuse) + spec;
在 technique 的改动上,我们保留了之前的光照着色通路,专门为纹理增加了新的光照着色通路:
// 之前的光照通路
...
technique11 Light2 {
pass P0 {
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(2, false)));
}
}
...
// 供纹理使用的光照通路
...
technique11 Light2Tex {
pass P0 {
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(2, true)));
}
}
...
在场景绘制函数 DrawScene() 中也没有过多改动:
void CrateApp::DrawScene() {
...
ID3DX11EffectTechnique *activeTech = Effects::BasicFX->Light2TexTech;
D3DX11_TECHNIQUE_DESC techDesc;
activeTech->GetDesc(&techDesc);
for (UINT p = 0; p < techDesc.Passes; ++p) {
md3dImmediateContext->IASetVertexBuffers(0, 1, &mBoxVB, &stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mBoxIB, DXGI_FORMAT_R32_UINT, 0);
// Draw the box.
XMMATRIX world = XMLoadFloat4x4(&mBoxWorld);
XMMATRIX worldInvTranspose = MathHelper::InverseTranspose(world);
XMMATRIX worldViewProj = world * view * proj;
Effects::BasicFX->SetWorld(world);
Effects::BasicFX->SetWorldInvTranspose(worldInvTranspose);
Effects::BasicFX->SetWorldViewProj(worldViewProj);
Effects::BasicFX->SetTexTransform(XMLoadFloat4x4(&mTexTransform));
Effects::BasicFX->SetMaterial(mBoxMat);
Effects::BasicFX->SetDiffuseMap(mDiffuseMapSRV);
activeTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mBoxIndexCount, mBoxIndexOffset,
mBoxVertexOffset);
}
HR(mSwapChain->Present(0, 0));
}
从代码中可以看出本程序是直接使用了 Light2Tex 作演示。