上一篇已經完成水波紋模型,但是只是在線框模式下能清晰的看到波動效果,實體填充時無法看出水面變化,主要原因就是沒有引入光照。這裏通過更改頂點着色器和像素着色器,引入水面的漫反射效果,讓整個模型更加真實。
爲簡化漫反射模型,假設光照射物體時,反射光會在物體表面均勻散開。這樣,無論觀察點在哪裏,總能看到反射光。物體表面漫反射光的顏色可以用蘭伯特餘弦定理進行計算,如下圖所示:
n是法向量,L是與光照方向相反的單位向量。n*L就是當前點的受光強度,再乘入射光的顏色就是實際照在物體表面的光,最後將其與物體本身顏色混合,得到漫反射後的表面顏色。計算光照可由GPU完成,即頂點着色器和像素着色器,所以明白算法之後開始更改HLSL代碼。
根據上面的公式,計算光照時需要用到法向量、光照方向、光照顏色及物體表面顏色。其中光照方向和光照顏色可以在像素着色器中定義,物體表面顏色已經在之前的文章中定義。只需在頂點着色器和像素着色器的輸入輸出增加法向量成員。代碼如下:
struct VertexShaderInput
{
float3 pos : POSITION;
float3 color : COLOR;
float3 normal : NORMAL;
};
struct VertexShaderOutput
{
float4 pos : SV_POSITION;
float3 color : COLOR;
float3 normal : NORMAL;
};
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float3 color : COLOR;
float3 normal : NORMAL;
};
頂點着色器只需將法向量轉換到世界空間並按公式要求歸一化,所以在其main方法裏添加如下代碼:
float4 normal = float4(input.normal, 0.0f);
normal = mul(normal, model);
output.normal = normalize(normal.xyz);
計算物體的最終顏色是像素着色器的任務。這裏設光照顏色爲(0.9,0.9,0.9),光照方向爲(1,-1,0),相當於從屏幕右上角斜45度入射。四個變量都已經定義,可以按照公式分三步計算出最終的顏色,代碼如下:
lightIntensity = saturate(dot(input.normal, lightDir));
finalColor = saturate(diffuseColor* lightIntensity);
finalColor = finalColor * float4(input.color,1.0f);
至此,GPU部分的代碼編寫完成。
之前的文章中提到,在主程序代碼中載入頂點着色器和像素着色器時,需要對其結構進行說明,接下來要做的就是修改主程序中涉及着色器的代碼。首先更改Direct3Dbase.h中結構體VertexPositionColor的定義,因爲在着色器中多出一個normal成員。
struct VertexPositionColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 color;
DirectX::XMFLOAT3 normal;
};
接着切換到Renderer.cpp文件,修改CreateDeviceResources方法中輸入佈局的定義:
const D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
然後更改山峯和水面模型中,負責生成頂點的代碼。如今多了一個normal成員,需要在生成頂點時一併算出其法向量。
山峯模型各頂點的法向量就是對其生成公式求導,可用一個方法專門計算。代碼如下:
XMFLOAT3 HillModel::GetHillNormal(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;
}
完成後在HillModel的Initialize方法裏添加以下代碼,生成法向量。
Vertices[xRange*row + col].normal = GetHillNormal(xPos,zPos);
水模型的法向量計算比較麻煩,因爲波動時水面的法向量方向也在變化。在WaterModel類裏添加一個mNormal成員專門保存水面的法向量。
在初始化水平面時,法向量肯定都平行於y軸方向,均設爲(0,1,0):
mNormal[xRange*row + col] = XMFLOAT3(0.0f, 1.0f, 0.0f);
在更新水面波動狀態時,除了計算水面各頂點的位置外,現在還要增加各頂點法向量的計算。在Update方法中增加以下代碼更新法向量緩衝區:
for(int row = 1; row < (xRange-1); ++row)
{
for(int col = 1; col < (zRange-1); ++col)
{
float l = mCurrSolution[row*xRange+col-1].y;
float r = mCurrSolution[row*xRange+col+1].y;
float t = mCurrSolution[(row-1)*xRange+col].y;
float b = mCurrSolution[(row+1)*xRange+col].y;
mNormal[row*xRange+col].x = -r+l;
mNormal[row*xRange+col].y = 2.0f*mSpatialStep;
mNormal[row*xRange+col].z = b-t;
XMVECTOR n = XMVector3Normalize(XMLoadFloat3(&mNormal[row*xRange+col]));
XMStoreFloat3(&mNormal[row*xRange+col], n);
}
}
最後將山峯模型和水模型的渲染模式設爲實體填充,顯示效果如下:
在現實世界中,光照模型不只包括漫反射光,還有其他像點光、平行光、高光等模型。現在實現的效果離實際情況還有很遠的距離,需要引入更多參數。
附本篇文章的源代碼: