最爱午后红茶

剖析带光照的头骨几何演示程序

日期图标
2023-06-21

带光照的头骨几何演示程序:

本程序是结合了 几何体演示程序从文件中加载模型 这两个程序并加以改动的。

主要改动有以下几点:

  • 使用实体模式渲染
  • 增加了 3 个光源,并可以使用按键(1、2、3)动态控制光源生效的数量
  • 内部结构的改动(适当地进行了一些结构封装)

三点布光

光源以及模型材质的初始化如下:

LitSkullApp::LitSkullApp(HINSTANCE hInstance)
    : D3DApp(hInstance), mShapesVB(0), mShapesIB(0), mSkullVB(0), mSkullIB(0),
      mSkullIndexCount(0), mLightCount(1), mEyePosW(0.0f, 0.0f, 0.0f),
      mTheta(1.5f * MathHelper::Pi), mPhi(0.1f * MathHelper::Pi),
      mRadius(15.0f) {
  ...
  // 主光源
  mDirLights[0].Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
  mDirLights[0].Diffuse = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
  mDirLights[0].Specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
  mDirLights[0].Direction = XMFLOAT3(0.57735f, -0.57735f, 0.57735f);

  // 补光源
  mDirLights[1].Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
  mDirLights[1].Diffuse = XMFLOAT4(0.20f, 0.20f, 0.20f, 1.0f);
  mDirLights[1].Specular = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);
  mDirLights[1].Direction = XMFLOAT3(-0.57735f, -0.57735f, 0.57735f);

  // 背光源
  mDirLights[2].Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
  mDirLights[2].Diffuse = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
  mDirLights[2].Specular = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
  mDirLights[2].Direction = XMFLOAT3(0.0f, -0.707f, -0.707f);

  mGridMat.Ambient = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
  mGridMat.Diffuse = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
  mGridMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);

  mCylinderMat.Ambient = XMFLOAT4(0.7f, 0.85f, 0.7f, 1.0f);
  mCylinderMat.Diffuse = XMFLOAT4(0.7f, 0.85f, 0.7f, 1.0f);
  mCylinderMat.Specular = XMFLOAT4(0.8f, 0.8f, 0.8f, 16.0f);

  mSphereMat.Ambient = XMFLOAT4(0.1f, 0.2f, 0.3f, 1.0f);
  mSphereMat.Diffuse = XMFLOAT4(0.2f, 0.4f, 0.6f, 1.0f);
  mSphereMat.Specular = XMFLOAT4(0.9f, 0.9f, 0.9f, 16.0f);

  mBoxMat.Ambient = XMFLOAT4(0.651f, 0.5f, 0.392f, 1.0f);
  mBoxMat.Diffuse = XMFLOAT4(0.651f, 0.5f, 0.392f, 1.0f);
  mBoxMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);

  mSkullMat.Ambient = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
  mSkullMat.Diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
  mSkullMat.Specular = XMFLOAT4(0.8f, 0.8f, 0.8f, 16.0f);
}

从代码可以看出,3 个光源都是平行光,且它们的方向如下:

三点布光
图一:三点布光

可见三个平行光源是按照三点布光(Three-Point Lighting)照明技术去设置的。简单来说,三点布光是一种常用的照明技术,广泛应用于摄影、电影制作和三维计算机图形等领域。它由三个主要光源组成,分别为主光源Key Light)、补光源Fill Light)和背光源Back Light),用于创造出具有阴影、层次感和立体感的照明效果。

以下是三个主要光源的功能和角色:

  • 主光源(Key Light):主光源是场景中的主要光源,通常位于物体的前方或侧面。它提供了主要的照明,并产生明暗对比,定义物体的形状和表面细节。主光源通常设置在相对较高的位置,以模拟太阳光的照射角度。
  • 补光源(Fill Light):补光源位于主光源的相对侧面,用于填充主光源所造成的阴影,并减轻阴影的对比度。补光源的亮度通常比主光源较弱,以确保仍然保留一定的阴影效果,同时使整体画面更加均衡。
  • 背光源(Back Light):背光源位于物体的背后,用于从背后照亮物体,产生轮廓和边缘的高光效果。它有助于将物体与背景分离,并为物体提供一定的立体感。背光源通常设置在相对较高的位置,以便在物体周围形成明亮的边缘光。

通过综合使用这三个光源,三点布光能够创造出具有深度、层次感和纹理的照明效果。这种技术可以使被拍摄或渲染的物体更加真实、立体和吸引人。

结构调整

随着 Demo 的愈加复杂,Effect 文件的读取以及它跟 C++ 的交互被封装成独立的类(不同的 Demo 程序可能会有不同的 Effect 实现,因此没有把它放到 Common 目录,而是每个程序单独实现):

Effect 类图
图二:Effect 类图

顶点和顶点输入布局也被重新封装:

namespace Vertex {
struct PosNormal {
  XMFLOAT3 Pos;
  XMFLOAT3 Normal;
};
} // namespace Vertex

class InputLayoutDesc {
public:
  // Init like const int A::a[4] = {0, 1, 2, 3}; in .cpp file.
  static const D3D11_INPUT_ELEMENT_DESC PosNormal[2];
};

class InputLayouts {
public:
  static void InitAll(ID3D11Device *device);
  static void DestroyAll();

  static ID3D11InputLayout *PosNormal;
};

其对应的实现为:

const D3D11_INPUT_ELEMENT_DESC InputLayoutDesc::PosNormal[2] = {
    {"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}};

ID3D11InputLayout *InputLayouts::PosNormal = 0;

void InputLayouts::InitAll(ID3D11Device *device) {
  //
  // PosNormal
  //

  D3DX11_PASS_DESC passDesc;
  Effects::BasicFX->Light1Tech->GetPassByIndex(0)->GetDesc(&passDesc);
  HR(device->CreateInputLayout(InputLayoutDesc::PosNormal, 2,
                               passDesc.pIAInputSignature,
                               passDesc.IAInputSignatureSize, &PosNormal));
}

void InputLayouts::DestroyAll() { ReleaseCOM(PosNormal); }

切换光源

在程序中使用按键(1、2、3)可以动态控制光源生效的数量。我们看下像素着色器的代码:

float4 PS(VertexOut pin, uniform int gLightCount) : 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;

  //
  // Lighting.
  //

  // 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;
  }

  float4 litColor = ambient + diffuse + spec;

  // Common to take alpha from diffuse material.
  litColor.a = gMaterial.Diffuse.a;

  return litColor;
}

从代码中可以看到,我们增加了一个参数 gLightCount 用以控制生效的光源数量。在这个程序中,我们使用了 3 个 technique:

technique11 Light1 {
  pass P0 {
    SetVertexShader(CompileShader(vs_5_0, VS()));
    SetGeometryShader(NULL);
    SetPixelShader(CompileShader(ps_5_0, PS(1)));
  }
}

technique11 Light2 {
  pass P0 {
    SetVertexShader(CompileShader(vs_5_0, VS()));
    SetGeometryShader(NULL);
    SetPixelShader(CompileShader(ps_5_0, PS(2)));
  }
}

technique11 Light3 {
  pass P0 {
    SetVertexShader(CompileShader(vs_5_0, VS()));
    SetGeometryShader(NULL);
    SetPixelShader(CompileShader(ps_5_0, PS(3)));
  }
}

可以看出,每个 technique 分别代表使用 1~3 个光源进行着色。而在 UpdateScene() 中,我们捕获了按键的结果:

void LitSkullApp::UpdateScene(float dt) {
  ...
  //
  // Switch the number of lights based on key presses.
  //
  if (GetAsyncKeyState('0') & 0x8000)
    mLightCount = 0;

  if (GetAsyncKeyState('1') & 0x8000)
    mLightCount = 1;

  if (GetAsyncKeyState('2') & 0x8000)
    mLightCount = 2;

  if (GetAsyncKeyState('3') & 0x8000)
    mLightCount = 3;
}

接着在 DrawScene() 中我们根据按键的结果选取对应的 technique 进行着色处理:

void LitSkullApp::DrawScene() {
  ...
  // Figure out which technique to use.
  // 默认是一个主光源
  ID3DX11EffectTechnique *activeTech = Effects::BasicFX->Light1Tech;
  switch (mLightCount) {
  case 1:
    activeTech = Effects::BasicFX->Light1Tech;
    break;
  case 2:
    activeTech = Effects::BasicFX->Light2Tech;
    break;
  case 3:
    activeTech = Effects::BasicFX->Light3Tech;
    break;
  }

  D3DX11_TECHNIQUE_DESC techDesc;
  activeTech->GetDesc(&techDesc);
  for (UINT p = 0; p < techDesc.Passes; ++p) {
    // 绘制处理
    ...
  }

  HR(mSwapChain->Present(0, 0));
}
* 未经同意不得转载。