最爱午后红茶

点光与聚光

日期图标
2023-06-20

点光

点光
图一:点光

从上图我们很容易理解,点光的光照向量会随着点的位置而变化(从目标点指向光源,跟光的传播方向相反),而平行光的光照向量则为常量。

点光的结构及其初始化如下:

struct PointLight {
  PointLight() { ZeroMemory(this, sizeof(this)); }

  XMFLOAT4 Ambient;
  XMFLOAT4 Diffuse;
  XMFLOAT4 Specular;

  // Packed into 4D vector: (Position, Range)
  XMFLOAT3 Position;
  float Range;

  // Packed into 4D vector: (A0, A1, A2, Pad)
  XMFLOAT3 Att;
  float
      Pad; // Pad the last float so we can set an array of lights if we wanted.
};

LightingApp::LightingApp(HINSTANCE hInstance)
    : D3DApp(hInstance), mLandVB(0), mLandIB(0), mWavesVB(0), mWavesIB(0),
      mFX(0), mTech(0), mfxWorld(0), mfxWorldInvTranspose(0), mfxEyePosW(0),
      mfxDirLight(0), mfxPointLight(0), mfxSpotLight(0), mfxMaterial(0),
      mfxWorldViewProj(0), mInputLayout(0), mEyePosW(0.0f, 0.0f, 0.0f),
      mTheta(1.5f * MathHelper::Pi), mPhi(0.1f * MathHelper::Pi),
      mRadius(80.0f) {
  ...
  // Point light--position is changed every frame to animate in UpdateScene
  // function.
  mPointLight.Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
  mPointLight.Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
  mPointLight.Specular = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
  mPointLight.Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
  mPointLight.Range = 25.0f;
  ...
}

其中,Att 表示衰减系数,我们知道 Shading point(或者说它周围的单位面积)在任何一个位置所能接收到的能量,跟光线传播的距离成平方反比(具体见 Games-101-Illumination 光的辐射相关知识):

  • I=I/r2I' = I/r^2

其中 II 表示跟光源距离为 1 处的能量。但是如果直接按照这个公式计算,通常得不到让人满意的效果,因此我们经常会使用另一个可调参的公式去代替:

  • I=Ia0+a1r+a2r2I' = \frac{I}{a_0 + a_{1}r + a_{2}r^2}

可以灵活地设置 Att = (a0,a1,a2)(a_0, a_1, a_2) 的值来取得不同的效果。

Range 则表示点光的照射范围。我们以一个点光照射在一个平面的截面分析:

点光的范围
图二:点光的范围

从上图我们可以看到,Range 其实就是点光朝四面八方发射光线的最长有效距离,从上图体现为圆的半径(实际上是球的半径,上图只是一个截面)。点 AA 处在 Range 内,所以参与光照的运算;而点 BB 在 Range 外,所以不参与光照的运算。如此一来,加上前面平行光作用下的山峰河谷的基础,点光部分的像素着色就很好理解了:

//---------------------------------------------------------------------------------------
// Computes the ambient, diffuse, and specular terms in the lighting equation
// from a point light.  We need to output the terms separately because
// later we will modify the individual terms.
//---------------------------------------------------------------------------------------
void ComputePointLight(Material mat, PointLight L, float3 pos, float3 normal,
                       float3 toEye, out float4 ambient, out float4 diffuse,
                       out float4 spec) {
  // Initialize outputs.
  ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
  diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
  spec = float4(0.0f, 0.0f, 0.0f, 0.0f);

  // The vector from the surface to the light.
  float3 lightVec = L.Position - pos;

  // The distance from surface to light.
  float d = length(lightVec);

  // Range test.
  if (d > L.Range)
    return;

  // Normalize the light vector.
  lightVec /= d;

  // Ambient term.
  ambient = mat.Ambient * L.Ambient;

  // Add diffuse and specular term, provided the surface is in
  // the line of site of the light.

  float diffuseFactor = dot(lightVec, normal);

  // Flatten to avoid dynamic branching.
  [flatten] if (diffuseFactor > 0.0f) {
    float3 v = reflect(-lightVec, normal);
    float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);

    diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
    spec = specFactor * mat.Specular * L.Specular;
  }

  // Attenuate
  float att = 1.0f / dot(L.Att, float3(1.0f, d, d * d));

  diffuse *= att;
  spec *= att;
}

整体演示 中我们发现,点光会绕着地形旋转。这部分逻辑的代码在 UpdateScene() 中:

void LightingApp::UpdateScene(float dt) {
  ...

  //
  // Animate the lights.
  //

  // Circle light over the land surface.
  mPointLight.Position.x = 70.0f * cosf(0.2f * mTimer.TotalTime());
  mPointLight.Position.z = 70.0f * sinf(0.2f * mTimer.TotalTime());
  mPointLight.Position.y =
      MathHelper::Max(
          GetHillHeight(mPointLight.Position.x, mPointLight.Position.z),
          -3.0f) +
      10.0f;
}

聚光

聚光的范围与强度
图三:聚光的范围与强度

聚光跟点光类似,但它跟前面的点光有三个区别:

  1. 聚光会把光线限制在一个圆锥体内
  2. 圆锥体内同一平面的点跟光的方向向量(上图 od\overrightarrow{od})越接近,能量越强(能量:A>B>C=0A > B > C = 0
  3. 最长有效距离(Range)通常比较大

通常我们会通过设置上图三 θ\theta 角的大小来指定一个聚光。那要怎样通过数学的方式来描述聚光的区域范围以及能量衰减呢?这里可以取巧去套用高光反射的公式(具体见 Games-101-Illumination 高光相关知识):

  • Ls=ks(I/r2)max(0,cosθ)p=ks(I/r2)max(0,nh)pL_s = k_{s}(I/r^2)max(0, cos{\theta})^p = k_{s}(I/r^2)max(0, \overrightarrow{n}\cdot\overrightarrow{h})^p

那么在 θ\theta 内的点都能参与光照运算,而且指数 pp 对应的是上述聚光与点光区别里面第二点的衰减速度(注意区别于从光源源头到目标点传播过程的衰减,它们是不一样的两个维度的衰减)。

聚光的结构与初始化如下:

struct SpotLight {
  SpotLight() { ZeroMemory(this, sizeof(this)); }

  XMFLOAT4 Ambient;
  XMFLOAT4 Diffuse;
  XMFLOAT4 Specular;

  // Packed into 4D vector: (Position, Range)
  XMFLOAT3 Position;
  float Range;

  // Packed into 4D vector: (Direction, Spot)
  XMFLOAT3 Direction;
  float Spot;

  // Packed into 4D vector: (Att, Pad)
  XMFLOAT3 Att;
  float
      Pad; // Pad the last float so we can set an array of lights if we wanted.
};

LightingApp::LightingApp(HINSTANCE hInstance)
    : D3DApp(hInstance), mLandVB(0), mLandIB(0), mWavesVB(0), mWavesIB(0),
      mFX(0), mTech(0), mfxWorld(0), mfxWorldInvTranspose(0), mfxEyePosW(0),
      mfxDirLight(0), mfxPointLight(0), mfxSpotLight(0), mfxMaterial(0),
      mfxWorldViewProj(0), mInputLayout(0), mEyePosW(0.0f, 0.0f, 0.0f),
      mTheta(1.5f * MathHelper::Pi), mPhi(0.1f * MathHelper::Pi),
      mRadius(80.0f) {
  ...
  // Spot light--position and direction changed every frame to animate in
  // UpdateScene function.
  mSpotLight.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
  mSpotLight.Diffuse = XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f);
  mSpotLight.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
  mSpotLight.Att = XMFLOAT3(1.0f, 0.0f, 0.0f); // 跟点光一样,表示光源源头到目标点的传播过程的衰减系数
  mSpotLight.Spot = 96.0f; // 相当于指数 p
  mSpotLight.Range = 10000.0f;
}

本程序的聚光的位置和方向跟相机的位置方向是完全一样的,也就是聚光会从相机位置射向世界空间中心,对应代码逻辑在 UpdateScene() 中:

void LightingApp::UpdateScene(float dt) {
  // Convert Spherical to Cartesian coordinates.
  float x = mRadius * sinf(mPhi) * cosf(mTheta);
  float z = mRadius * sinf(mPhi) * sinf(mTheta);
  float y = mRadius * cosf(mPhi);

  mEyePosW = XMFLOAT3(x, y, z);

  // Build the view matrix.
  XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
  XMVECTOR target = XMVectorZero();
  XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

  XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
  XMStoreFloat4x4(&mView, V);

  // The spotlight takes on the camera position and is aimed in the
  // same direction the camera is looking.  In this way, it looks
  // like we are holding a flashlight.
  mSpotLight.Position = mEyePosW;
  XMStoreFloat3(&mSpotLight.Direction, XMVector3Normalize(target - pos));
}

整体演示 中我们也可以直观地看到这一点。

聚光部分的像素着色如下:

//---------------------------------------------------------------------------------------
// Computes the ambient, diffuse, and specular terms in the lighting equation
// from a spotlight.  We need to output the terms separately because
// later we will modify the individual terms.
//---------------------------------------------------------------------------------------
void ComputeSpotLight(Material mat, SpotLight L, float3 pos, float3 normal,
                      float3 toEye, out float4 ambient, out float4 diffuse,
                      out float4 spec) {
  // Initialize outputs.
  ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
  diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
  spec = float4(0.0f, 0.0f, 0.0f, 0.0f);

  // The vector from the surface to the light.
  float3 lightVec = L.Position - pos;

  // The distance from surface to light.
  float d = length(lightVec);

  // Range test.
  if (d > L.Range)
    return;

  // Normalize the light vector.
  lightVec /= d;

  // Ambient term.
  ambient = mat.Ambient * L.Ambient;

  // Add diffuse and specular term, provided the surface is in
  // the line of site of the light.

  float diffuseFactor = dot(lightVec, normal);

  // Flatten to avoid dynamic branching.
  [flatten] if (diffuseFactor > 0.0f) {
    float3 v = reflect(-lightVec, normal);
    float specFactor = pow(max(dot(v, toEye), 0.0f), mat.Specular.w);

    diffuse = diffuseFactor * mat.Diffuse * L.Diffuse;
    spec = specFactor * mat.Specular * L.Specular;
  }

  // Scale by spotlight factor and attenuate.
  float spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);

  // Scale by spotlight factor and attenuate.
  float att = spot / dot(L.Att, float3(1.0f, d, d * d));

  ambient *= spot;
  diffuse *= att;
  spec *= att;
}
* 未经同意不得转载。