在真實環境中,同一個物體在不同光源照射下的顏色並不一樣,因爲物體本身並沒有顏色,而是它會反射不同顏色的光。物體對不同顏色光的吸收率、反射率,加上光澤度、透明度等其他物理屬性組合在一起,定義了這個物體的材質。知道物體的材質,就能夠方便地算出物體在不同光源照射下的顏色。這裏簡化山峯模型,統一使用陸地材質,水面則使用水材質,增加了平行光源、點光源和聚光燈三種光照模式,模擬一個更通用的山峯水波模型。實現流程和漫反射光實現基本一樣。
首先編寫着色器代碼。平行光、點光和聚光燈均需要自己的數據結構和計算方法,可在一個頭文件中進行定義。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所示。藍圈是聚光燈效果,紅圈是點光源效果。這個模型看起來並不美觀,像是塑料做的,因爲只用了兩種材質,而且沒有細緻調整材質參數。不過,通過實現這個模型,可以加深對光照的理解。結合後面的紋理等內容,才能做出更真實的模型。
本篇文章的源代碼: