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

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