上一篇已经完成水波纹模型,但是只是在线框模式下能清晰的看到波动效果,实体填充时无法看出水面变化,主要原因就是没有引入光照。这里通过更改顶点着色器和像素着色器,引入水面的漫反射效果,让整个模型更加真实。
为简化漫反射模型,假设光照射物体时,反射光会在物体表面均匀散开。这样,无论观察点在哪里,总能看到反射光。物体表面漫反射光的颜色可以用兰伯特余弦定理进行计算,如下图所示:
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);
}
}
最后将山峰模型和水模型的渲染模式设为实体填充,显示效果如下:
在现实世界中,光照模型不只包括漫反射光,还有其他像点光、平行光、高光等模型。现在实现的效果离实际情况还有很远的距离,需要引入更多参数。
附本篇文章的源代码: