最爱午后红茶

剖析板条箱演示程序

日期图标
2023-06-23

板条箱演示程序:

本程序是在 立方体演示程序 的基础上做改动。主要改动为:

  • 增加了两个光源
  • 给立方体每个面贴上一层纹理

注意:理解本程序内容需要有 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° 角打在顶面与左面;第二个平行光跟对角线 ABAB 平行且由 AA 指向 BB(也就是主要受光面是正面和右面):

板条箱的两束平行光方向
图一:板条箱的两束平行光方向

由演示中我们也可以看出,底面和背面由于没有接收到直接光照,因此是暗淡的(但是因为有环境光,所以没有全黑)。

在 DirectX 中,纹理坐标系的 vv 轴正方向是向下的,跟 Games-101 中的描述相反,但是不影响理解。

板条纹理
图二:板条纹理

初始化

CrateApp 初始化的过程中主要做了两个事情:

  1. 从文件中读取纹理数据
  2. 创建模型及其顶点属性
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() 的调用做了两个事情:

  1. 从文件中读取图像数据(支持 BMP、JPG、PNG、DDS、TIFF、GIF、WMP(参见 D3DX11_IMAGE_FILE_FORMAT))
  2. 为纹理创建对应的着色器资源视图(纹理资源需要通过视图才能绑定到渲染管线上)

上述两个过程也可以通过分别调用 D3DX11CreateTextureFromFile()CreateShaderResourceView() 来完成。

DDSDirectDraw 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++ 中的纹理资源做交互,使得着色器中的代码可以访问纹理资源。另外我们还给 VertexInVertexOut 结构增加了纹理坐标字段供着色使用。

顶点着色器并没有太大的改动,只是增加了纹理坐标的计算:

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",与前一行类似。
};

字段 AddressUAddressV 指定寻址模式,具体见:寻址模式

在根据光源数量计算出光照的影响后,我们把纹理颜色作为材质的属性并将其整合到最终的颜色里面(不需要包括高光):

// 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 作演示。

* 未经同意不得转载。