D3D12渲染技术之光源

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxw167/article/details/82824169

灯光有Point光源,Spot光源,Directional光源,Area光源等等,网上这方面的文章很多,在此我们就不详细介绍每个光源的计算公式了,我们直接介绍将光源应用到我们的案例中。先看我们已经实现好的案例截图:
在这里插入图片描述

顶点格式

照明计算需要表面法线, 我们使用顶点定义法线,然后在三角形的像素上插入这些法线,以便我们可以对每个像素进行光照计算。 而且,我们不再指定顶点颜色, 相反,通过对每个像素应用照明方程来生成像素颜色, 为了支持顶点法线,我们修改我们的顶点结构,如下所示:

// C++ Vertex structure
struct Vertex
{
  DirectX::XMFLOAT3 Pos;
  DirectX::XMFLOAT3 Normal;
};
 
// Corresponding HLSL vertex structure
struct VertexIn
{
  float3 PosL  : POSITION;
  float3 NormalL : NORMAL;
};
When we add a new vertex format, we need to describe it with a new input layout description:
mInputLayout =,
{
  { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
  { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, 
   D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

法线计算

GeometryGenerator中的形状函数已经创建了具有顶点法线的数据,因此我们都设置在那里。 但是,因为我们在这个演示中修改网格的高度使其看起来像地形,我们需要自己为地形生成法线向量。
因为我们的地形表面由函数y = f(x,z)给出,我们可以使用微积分直接计算法向量,而不是前面描述的常规平均计算。 对于曲面上的每个点,我们通过取偏导数在+ x-和+ z-方向上形成两个切向量:
在这里插入图片描述
这两个矢量位于表面点的切平面中, 计算法线向量得到:
在这里插入图片描述
我们用于生成地形网格的函数是:
在这里插入图片描述
偏导数是:
在这里插入图片描述
因此,表面点(x,f(x,z),z)处的表面法线由下式给出:
在这里插入图片描述
我们注意到这个曲面法线不是单位长度,因此需要在光照计算之前进行标准化。特别是,我们在每个顶点处进行上述常规计算以获得顶点法线:

XMFLOAT3 LitWavesApp::GetHillsNormal(float x, float z)const
{
  // n = (-df/dx, 1, -df/dz)
  XMFLOAT3 n( -0.03f*z*cosf(0.1f*x) - 0.3f*cosf(0.1f*z),
    1.0f,
    -0.3f*sinf(0.1f*x) + 0.03f*x*sinf(0.1f*z));
 
  XMVECTOR unitNormal = XMVector3Normalize(XMLoadFloat3(&n));
  XMStoreFloat3(&n, unitNormal);
 
  return n;
}

水面的法向矢量以类似的方式完成, 可以使用有限差分方案来近似每个顶点处的切向量。

更新灯光方向

我们的Lights数组被放入per-pass常量缓冲区, 该案例使用一个方向灯来表示太阳,并允许用户使用向左,向右,向上和向下箭头键旋转太阳位置。 因此,每一帧,我们都需要从太阳计算新的光方向,并将其设置为通道常量缓冲区。
我们以球坐标(ρ,θ,φ)跟踪太阳位置,但半径ρ无关紧要,因为我们假设太阳是无限远的。 特别是,我们只使用ρ= 1使其位于单位球面上并将(1,θ,φ)解释为朝向太阳的方向,以下是更新太阳的相关代码。

float mSunTheta = 1.25f*XM_PI;
float mSunPhi = XM_PIDIV4;
 
void LitWavesApp::OnKeyboardInput(const GameTimer& gt)
{
  const float dt = gt.DeltaTime();
 
  if(GetAsyncKeyState(VK_LEFT) & 0x8000)
    mSunTheta -= 1.0f*dt;
 
  if(GetAsyncKeyState(VK_RIGHT) & 0x8000)
    mSunTheta += 1.0f*dt;
 
  if(GetAsyncKeyState(VK_UP) & 0x8000)
    mSunPhi -= 1.0f*dt;
    if(GetAsyncKeyState(VK_DOWN) & 0x8000)
    mSunPhi += 1.0f*dt;
 
  mSunPhi = MathHelper::Clamp(mSunPhi, 0.1f, XM_PIDIV2);
}
 
void LitWavesApp::UpdateMainPassCB(const GameTimer& gt)
{
    …
  XMVECTOR lightDir = -MathHelper::SphericalToCartesian(1.0f, mSunTheta, mSunPhi);
 
  XMStoreFloat3(&mMainPassCB.Lights[0].Direction, lightDir);
  mMainPassCB.Lights[0].Strength = { 0.8f, 0.8f, 0.7f };
 
  auto currPassCB = mCurrFrameResource->PassCB.get();
  currPassCB->CopyData(0, mMainPassCB);
}
 

Light数组放入per-pass常量缓冲区意味着每个渲染通道不能超过16个(我们支持的最大光数)灯, 这对于Demo来说已经足够了。 然而,对于大型游戏世界来说,这还不够,因为你可以想象有数百个灯光的游戏关卡遍布整个关卡, 解决此问题的一种方法是将Light数组移动到每个对象的常量缓冲区。 然后,对于每个对象,搜索场景并找到影响对象的灯光,并将这些灯光绑定到常量缓冲区。 影响对象的灯光是其体积(点光源和聚光灯锥体)与其相交的灯光, 另一种流行的策略是使用延迟渲染或前向+渲染。

Lighting为我们的着色器程序引入了一个新的材质常量缓冲区, 为了支持这一点,我们需要更新根签名以支持额外的常量缓冲区。 与每个对象常量缓冲区一样,我们使用材质常量缓冲区的根描述符来支持直接绑定常量缓冲区,而不是通过描述符堆。

代码下载地址:链接:https://pan.baidu.com/s/1X0Vikf6qGYGPKU-Nwf-wYA 密码:h79q

阅读更多

扫码向博主提问

海洋_

博客专家

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • 3D引擎架构
  • 服务器架构
  • GPU渲染
  • 客户端架构
  • 引擎优化
去开通我的Chat快问

没有更多推荐了,返回首页