在真实环境中,同一个物体在不同光源照射下的颜色并不一样,因为物体本身并没有颜色,而是它会反射不同颜色的光。物体对不同颜色光的吸收率、反射率,加上光泽度、透明度等其他物理属性组合在一起,定义了这个物体的材质。知道物体的材质,就能够方便地算出物体在不同光源照射下的颜色。这里简化山峰模型,统一使用陆地材质,水面则使用水材质,增加了平行光源、点光源和聚光灯三种光照模式,模拟一个更通用的山峰水波模型。实现流程和漫反射光实现基本一样。
首先编写着色器代码。平行光、点光和聚光灯均需要自己的数据结构和计算方法,可在一个头文件中进行定义。HLSL的头文件扩展名为hlsli,创建方法与创建C++头文件一样,其代码如下:
/***************************************************
/ LightBase.hlsli
/
/ 光源结构体和对应的光照计算方法。
/***************************************************/
struct DirectionalLight
{
float4 Ambient;
float4 Diffuse;
float4 Specular;
float3 Direction;
float pad;
};
struct PointLight
{
float4 Ambient;
float4 Diffuse;
float4 Specular;
float3 Position;
float Range;
float3 Att;
float pad;
};
struct SpotLight
{
float4 Ambient;
float4 Diffuse;
float4 Specular;
float3 Position;
float Range;
float3 Direction;
float Spot;
float3 Att;
float pad;
};
struct Material
{
float4 Ambient;
float4 Diffuse;
float4 Specular;
float4 Reflect;
};
//---------------------------------------------------------------------------------------
// 计算平行光源照射产生的环境光,漫反射光和高光
//---------------------------------------------------------------------------------------
void ComputeDirectionalLight(Material mat, DirectionalLightL,
float3 normal, float3 toEye,
out float4 ambient,
out float4 diffuse,
out float4 spec)
{
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);
// V向量的方向与光线的方向相反.
float3 lightVec =-L.Direction;
// 计算环境光
ambient =mat.Ambient * L.Ambient;
// 计算漫反射光和高光
// 如果V向量与法向量夹角小于零,无需计算
float diffuseFactor =dot(lightVec, normal);
[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;
}
}
//---------------------------------------------------------------------------------------
// 计算点光源照射产生的环境光,漫反射光和高光
//---------------------------------------------------------------------------------------
void ComputePointLight(Material mat, PointLight L, float3 pos, float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
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);
// 点光源指向物体表面的光线向量
float3 lightVec =L.Position - pos;
// 点光源到物体表面的距离,用于计算衰减
float d =length(lightVec);
// 超出点光源照射范围不计算
if( d > L.Range )
return;
// 归一化光线向量
lightVec /= d;
// 计算环境光
ambient = mat.Ambient* L.Ambient;
// 计算漫反射光和高光
// 如果V向量与法向量夹角小于零,无需计算
float diffuseFactor =dot(lightVec, normal);
[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;
}
// 计算衰减
float att = 1.0f /dot(L.Att, float3(1.0f, d, d*d));
diffuse *= att;
spec *= att;
}
//---------------------------------------------------------------------------------------
// 计算聚光灯光源照射产生的环境光,漫反射光和高光
//---------------------------------------------------------------------------------------
void ComputeSpotLight(Material mat, SpotLight L, float3 pos, float3 normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4 spec)
{
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);
// 点光源指向物体表面的光线向量
float3 lightVec =L.Position - pos;
// 点光源到物体表面的距离,用于计算衰减
float d =length(lightVec);
// 超出点光源照射范围不计算
if( d > L.Range )
return;
// 归一化光线向量
lightVec /= d;
// 计算环境光
ambient =mat.Ambient * L.Ambient;
// 计算漫反射光和高光
// 如果V向量与法向量夹角小于零,无需计算
float diffuseFactor =dot(lightVec, normal);
[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;
}
// 根据向量夹角计算接收光强,夹角越小,光强越强
float spot =pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
// 根据光源与物体表面的距离计算衰减
float att = spot /dot(L.Att, float3(1.0f, d, d*d));
ambient *= spot;
diffuse *= att;
spec *= att;
}
这部分是光照计算的核心。三个方法能够根据给定的材质、光源位置和像素点位置,计算出三种光源的照射效果。关于算法的详细介绍可参照DirectX 10游戏编程入门。虽然DirectX的版本不同,但是基本原理都是一样的,所以DirectX 10里的光照算法同样适用于DirectX 11,只是在一些细节上需要改变。
有了核心算法后,像素着色器就可以调用这些方法,计算当前像素的最终颜色:
#include "LightBase.hlsli"
cbuffer ConstantLightBuffer : register(b0)
{
DirectionalLightgDirLight;
PointLightgPointLight;
SpotLightgSpotLight;
float3 gEyePosW;
float pad;
MaterialgMaterial;
};
struct PixelShaderInput
{
float4 posH : SV_POSITION;
float3 posW : POSITION;
float3 normal : NORMAL;
};
float4 main(PixelShaderInput input) : SV_TARGET
{
input.normal =normalize(input.normal);
float3 toEyeW =normalize(gEyePosW - input.posW);
// 初始化
float4 ambient = float4(0.0f, 0.0f, 0.0f,0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f,0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// 累加各个光源的照射效果
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLight, input.normal, toEyeW, A,D, S);
ambient +=A;
diffuse += D;
spec += S;
ComputePointLight(gMaterial,gPointLight, input.posW, input.normal, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
ComputeSpotLight(gMaterial,gSpotLight, input.posW, input.normal, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
float4 finalColor =ambient + diffuse + spec;
// 设置透明度
finalColor.a =diffuse.a;
return finalColor;
}
注意像素着色器代码开头定义的常量缓冲区ConstantLightBuffer。ConstantLightBuffer包含三种光源、观察点位置和材质信息,可以在程序中动态更新,方便实现动画效果。完成像素着色器后就要更新顶点着色器。顶点着色器的代码变化不大,只是需要在VertexShaderOutput中增加一个成员,用于光照效果计算。代码如下:
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
matrix model;
matrix view;
matrix projection;
};
struct VertexShaderInput
{
float3 pos : POSITION;
float3 normal : NORMAL;
};
struct VertexShaderOutput
{
float4 posH : SV_POSITION;
float3 posW : POSITION;
float3 normal : NORMAL;
};
VertexShaderOutput main(VertexShaderInput input)
{
VertexShaderOutputoutput;
float4 pos = float4(input.pos, 1.0f);
// 转换点坐标到投影空间
pos = mul(pos,model);
pos = mul(pos,view);
pos = mul(pos,projection);
output.posH =pos;
// 用世界空间进行光照计算
output.posW =mul(float4(input.pos, 1.0f),model).xyz;
// 转换法向量到世界空间并归一化
float4 normal = float4(input.normal,0.0f);
normal =mul(normal, model);
output.normal =normalize(normal.xyz);
return output;
}
HLSL代码完成后就要转到C++代码。因为有部分结构是GPU和CPU共用的,所以必须保证HLSL里定义的结构与C++中的完全一致。新建一个C++头文件LightHelper.h,其内容与DirectX 10游戏编程入门的示例完全一样,只是需要增加两行语句,用来包含新的头文件和设置命名空间:
//***************************************************************************************
// LightHelper.h
//
// 光源及材质结构定义
//***************************************************************************************
#ifndef LIGHTHELPER_H
#define LIGHTHELPER_H
#include <DirectXHelper.h>
using namespace DirectX;
struct DirectionalLight
{
DirectionalLight(){ ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Direction;
float Pad;
};
struct PointLight
{
PointLight() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Att;
float Pad;
};
struct SpotLight
{
SpotLight() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Direction;
float Spot;
XMFLOAT3 Att;
float Pad;
};
struct Material
{
Material() { ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT4 Reflect;
};
#endif // LIGHTHELPER_H
还要在Direct3DBase.h里修改VertexPositionColor结构体。因为定义材质后,不需要指定顶点颜色,所以删除其color成员,并更名为VertexPosition。然后再增加光照常量缓冲区定义。代码如下:
struct VertexPosition
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT3 normal;
};
struct ConstantLightBuffer{
DirectionalLight gDirLight;
PointLight gPointLight;
SpotLight gSpotLight;
XMFLOAT3 gEyePosW;
float pad;
Material gMaterial;
};
注意,VertexPositionColor更改后,还需在使用它的HillModel类、WaterModel类里删除与color成员相关的代码,并更改输入布局。完成这些准备工作后,就可以开始为模型添加光照效果。
首先在Renderer类里增加光照和材质成员:
ConstantLightBuffer m_constantLightBufferData;
Material m_landMat;
Material m_wavesMat;
然后在其初始化方法CreateDeviceResources中增加初始化代码:
CD3D11_BUFFER_DESC constantLightBufferDesc(sizeof(ConstantLightBuffer), D3D11_BIND_CONSTANT_BUFFER);
DX::ThrowIfFailed(
m_d3dDevice->CreateBuffer(
&constantLightBufferDesc,
nullptr,
&m_constantLightBuffer
)
);
// 平行光初始化
m_constantLightBufferData.gDirLight.Ambient = XMFLOAT4(0.2f, 0.2f, 0.2f, 1.0f);
m_constantLightBufferData.gDirLight.Diffuse = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_constantLightBufferData.gDirLight.Specular = XMFLOAT4(0.5f, 0.5f, 0.5f,1.0f);
m_constantLightBufferData.gDirLight.Direction = XMFLOAT3(0.57735f,-0.57735f, 0.57735f);
// 点光初始化
m_constantLightBufferData.gPointLight.Ambient = XMFLOAT4(0.3f, 0.3f, 0.3f, 1.0f);
m_constantLightBufferData.gPointLight.Diffuse = XMFLOAT4(0.7f, 0.7f, 0.7f, 1.0f);
m_constantLightBufferData.gPointLight.Specular = XMFLOAT4(0.7f, 0.7f, 0.7f,1.0f);
m_constantLightBufferData.gPointLight.Att = XMFLOAT3(0.0f, 0.1f, 0.0f);
m_constantLightBufferData.gPointLight.Range = 25.0f;
// 聚光灯初始化
m_constantLightBufferData.gSpotLight.Ambient = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
m_constantLightBufferData.gSpotLight.Diffuse = XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f);
m_constantLightBufferData.gSpotLight.Specular = XMFLOAT4(1.0f, 1.0f, 1.0f,1.0f);
m_constantLightBufferData.gSpotLight.Att = XMFLOAT3(1.0f, 0.0f, 0.0f);
m_constantLightBufferData.gSpotLight.Spot = 50.0f;
m_constantLightBufferData.gSpotLight.Range = 10000.0f;
// 定义陆地材质
m_landMat.Ambient = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
m_landMat.Diffuse = XMFLOAT4(0.48f, 0.77f, 0.46f, 0.0f);
m_landMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f,16.0f);
// 定义水波材质
m_wavesMat.Ambient = XMFLOAT4(0.137f, 0.42f, 0.556f, 1.0f);
m_wavesMat.Diffuse = XMFLOAT4(0.137f, 0.42f, 0.556f, 0.0f);
m_wavesMat.Specular = XMFLOAT4(0.8f, 0.8f, 0.8f,96.0f);
注意,这里并没有初始化点光源和聚光灯的位置,因为想让点光源以(50,30,50)为圆心,30为半径在XZ平面做圆周运动,而聚光灯则照向观察方向,所以不进行初始化,而是在Update方法里更新它们的位置:
XMFLOAT3 eyePos = XMFLOAT3(15.0f, 30.0f, 15.0f);
XMFLOAT3 pointLightPos = XMFLOAT3(30.0f*sinf(0.5f*timeTotal)+50.0f, 30.0f,30.0f*cosf(0.5f*timeTotal)+50.0f);
m_constantLightBufferData.gEyePosW = eyePos;
m_constantLightBufferData.gPointLight.Position =pointLightPos;
m_constantLightBufferData.gSpotLight.Position = eyePos;
XMStoreFloat3(&m_constantLightBufferData.gSpotLight.Direction,XMVector3Normalize(at - eye));
最后是渲染部分。在Render方法里添加以下代码:
m_d3dContext->PSSetConstantBuffers(
0,
1,
m_constantLightBuffer.GetAddressOf()
);
// 渲染陆地材质
m_constantLightBufferData.gMaterial = m_landMat;
m_d3dContext->UpdateSubresource(
m_constantLightBuffer.Get(),
0,
NULL,
&m_constantLightBufferData,
0,
0
);
m_hill.Render(m_d3dContext.Get());
// 渲染水材质
m_constantLightBufferData.gMaterial = m_wavesMat;
m_d3dContext->UpdateSubresource(
m_constantLightBuffer.Get(),
0,
NULL,
&m_constantLightBufferData,
0,
0
);
m_water.Render(m_d3dContext.Get());
因为只有像素着色器使用光照常量缓冲区,所以在开头用PSSetConstantBuffers方法设置光照常量缓冲区。更新数据时则用UpdateSubresource方法。注意,哪怕只更新缓冲区的一个成员,也要更新整个缓冲区。
运行效果如图1所示。蓝圈是聚光灯效果,红圈是点光源效果。这个模型看起来并不美观,像是塑料做的,因为只用了两种材质,而且没有细致调整材质参数。不过,通过实现这个模型,可以加深对光照的理解。结合后面的纹理等内容,才能做出更真实的模型。
本篇文章的源代码: