Windows 8 Directx 開發學習筆記(九)材質定義及混合光照效果實現

在真實環境中,同一個物體在不同光源照射下的顏色並不一樣,因爲物體本身並沒有顏色,而是它會反射不同顏色的光。物體對不同顏色光的吸收率、反射率,加上光澤度、透明度等其他物理屬性組合在一起,定義了這個物體的材質。知道物體的材質,就能夠方便地算出物體在不同光源照射下的顏色。這裏簡化山峯模型,統一使用陸地材質,水面則使用水材質,增加了平行光源、點光源和聚光燈三種光照模式,模擬一個更通用的山峯水波模型。實現流程和漫反射光實現基本一樣。

首先編寫着色器代碼。平行光、點光和聚光燈均需要自己的數據結構和計算方法,可在一個頭文件中進行定義。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;
}

注意像素着色器代碼開頭定義的常量緩衝區ConstantLightBufferConstantLightBuffer包含三種光源、觀察點位置和材質信息,可以在程序中動態更新,方便實現動畫效果。完成像素着色器後就要更新頂點着色器。頂點着色器的代碼變化不大,只是需要在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所示。藍圈是聚光燈效果,紅圈是點光源效果。這個模型看起來並不美觀,像是塑料做的,因爲只用了兩種材質,而且沒有細緻調整材質參數。不過,通過實現這個模型,可以加深對光照的理解。結合後面的紋理等內容,才能做出更真實的模型。


本篇文章的源代碼:

Direct3DApp_HillWaveFinal

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章