D3D12渲染技術之光源
燈光有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為我們的著色器程式引入了一個新的材質常量緩衝區, 為了支援這一點,我們需要更新根簽名以支援額外的常量緩衝區。 與每個物件常量緩衝區一樣,我們使用材質常量緩衝區的根描述符來支援直接繫結常量緩衝區,而不是通過描述符堆。