第十七章 Lights

第十七章 Lights

本章將會開發一組類函數用於支持directional lights、point lights以及spotlights。這項工作完成了C++渲染引擎框的全部基礎內容,並標誌着第三部分“Rendering with DirectX”的結束。

Motivation

在第6章“Lighting Models”和第7章“Additional Lighting Models”,我們花了大量的精力編寫用於模擬光照的shaders。具體包括如下幾種光照模型:
Ambient lighting(環境光)
Diffuse lighting with directional lights(由方向光源產生的漫反射光)
Specular highlights(鏡面光)
Point lights(點光源)
Spotlights(聚光燈)
我們需要一種在C++渲染引擎框架中表示這些光照模型的方法。另外,需要創建一系列materials用於封裝應用程序與這些光照effects的交互,這就是本章需要完成的工作。最後使用這類material類編寫了一系列的示例程序,用於模擬實時光照渲染。

Light Data Types

在前面所編寫的shaders中已經模擬了三種不同的光照“類型”:directional、point和spotlights。這三種光照模型中都包含有一個color值,因此可以編寫一個Light基類,包含一個成員變量表示color。另外,在directional和point lights中不包含任何共同的數據。一個directional light照射一個特定的方向,但沒有座標位置(因此也不會衰減)。相反,一個point light具有一個座標位置和一個輻射半徑,但是沒有特定的方向。因此,可以想象把這兩種光源分別使用DirectionalLight和PointLight表示,這兩個類都由Light基類派生。最後,一個spotlight中同時包含directional和point lights;除了一個座標位置和一個輻射半徑,還會以一個特定的方向照射。你可能會想象編寫一個SpotLight繼承自DirectionalLight和PointLight類。但是,我傾向於避免使用多重繼承,因此SpotLight只由PointLinght類派生,並重新創建directional light中的相關成員。
圖17.1中顯示了這幾種光源類型的類結構圖。


圖17.1 Class diagrams for the light data types.
在這個幾個類的幾乎沒有任何複雜的實現,因此在這裏沒有列出代碼。在本書的配套網站上提供了全部的代碼。

A Diffuse Lighting Material

回顧一下在第6章編寫的diffuse lighting shader。其中包含了一系列shader constants用於表示ambient color、light color以及light direction,還有一個color texture,worldviewprojection和world矩陣。把該shader對應的effect文件(DiffuseLight.fx)添加工程中,並創建一個DiffuseLightingMaterial類,以及DiffuseLightingMaterialVertex結構體,該類的聲明代碼如列表17.1所示。
列表17.1 Declarations of Diffuse Lighting Material and Vertex Data Types
#pragma once

#include "Common.h"
#include "Material.h"

using namespace Library;

namespace Rendering
{
    typedef struct _DiffuseLightingMaterialVertex
    {
        XMFLOAT4 Position;
        XMFLOAT2 TextureCoordinates;
        XMFLOAT3 Normal;

        _DiffuseLightingMaterialVertex() { }

        _DiffuseLightingMaterialVertex(XMFLOAT4 position, XMFLOAT2 textureCoordinates, XMFLOAT3 normal)
            : Position(position), TextureCoordinates(textureCoordinates), Normal(normal) { }
    } DiffuseLightingMaterialVertex;

    class DiffuseLightingMaterial : public Material
    {
        RTTI_DECLARATIONS(DiffuseLightingMaterial, Material)

        MATERIAL_VARIABLE_DECLARATION(WorldViewProjection)
        MATERIAL_VARIABLE_DECLARATION(World)
        MATERIAL_VARIABLE_DECLARATION(AmbientColor)
        MATERIAL_VARIABLE_DECLARATION(LightColor)
        MATERIAL_VARIABLE_DECLARATION(LightDirection)
        MATERIAL_VARIABLE_DECLARATION(ColorTexture)

    public:
        DiffuseLightingMaterial();		

        virtual void Initialize(Effect* effect) override;
        virtual void CreateVertexBuffer(ID3D11Device* device, const Mesh& mesh, ID3D11Buffer** vertexBuffer) const override;
        void CreateVertexBuffer(ID3D11Device* device, DiffuseLightingMaterialVertex* vertices, UINT vertexCount, ID3D11Buffer** vertexBuffer) const;
        virtual UINT VertexSize() const override;
    };
}


注意
我們並沒有忘記添加ambient lighting!只是因爲在diffuse lighting effect中包含了ambient lighting,因此在這個C++渲染框架中沒有特地演示。在本書的配套網站上提供了一個單獨的ambient lighting示例。

在diffuse lighting effect中每一個vertex都由一個position、UV以及normal成員組成;通過DiffuseLightingMaterialVertex結構體可以表示這些成員。在DiffuseLightingMaterial類中通過使用MATERAIL_VARIABLE_DECLARATION宏聲明瞭這些shader constants對應的成員變量,並提供了訪問這些變量的公有函數。因此,該類的成員列表比上一章編寫的類要更長一些,但是聲明方法是一樣的。同樣,在DiffuseLightingMaterial類的實現代碼中也是如此。爲了理解熟悉材質系統中宏聲明的語法,列表17.2中列出了實現代碼中完整的成員列表。但是,這也是本章最後一次列出這些成員列表。所有剩下的materials中都會使用這種編碼方法。
列表17.2 The DiffuseLightingMaterial.cpp File

#include "DiffuseLightingMaterial.h"
#include "GameException.h"
#include "Mesh.h"

namespace Rendering
{
    RTTI_DEFINITIONS(DiffuseLightingMaterial)

    DiffuseLightingMaterial::DiffuseLightingMaterial()
        : Material("main11"),
          MATERIAL_VARIABLE_INITIALIZATION(WorldViewProjection), MATERIAL_VARIABLE_INITIALIZATION(World),
          MATERIAL_VARIABLE_INITIALIZATION(AmbientColor), MATERIAL_VARIABLE_INITIALIZATION(LightColor),
          MATERIAL_VARIABLE_INITIALIZATION(LightDirection), MATERIAL_VARIABLE_INITIALIZATION(ColorTexture)
    {
    }

    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, WorldViewProjection)
    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, World)
    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, AmbientColor)
    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, LightColor)
    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, LightDirection)
    MATERIAL_VARIABLE_DEFINITION(DiffuseLightingMaterial, ColorTexture)

    void DiffuseLightingMaterial::Initialize(Effect* effect)
    {
        Material::Initialize(effect);

        MATERIAL_VARIABLE_RETRIEVE(WorldViewProjection)
        MATERIAL_VARIABLE_RETRIEVE(World)
        MATERIAL_VARIABLE_RETRIEVE(AmbientColor)
        MATERIAL_VARIABLE_RETRIEVE(LightColor)
        MATERIAL_VARIABLE_RETRIEVE(LightDirection)
        MATERIAL_VARIABLE_RETRIEVE(ColorTexture)

        D3D11_INPUT_ELEMENT_DESC inputElementDescriptions[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }
        };

        CreateInputLayout("main11", "p0", inputElementDescriptions, ARRAYSIZE(inputElementDescriptions));
    }

    void DiffuseLightingMaterial::CreateVertexBuffer(ID3D11Device* device, const Mesh& mesh, ID3D11Buffer** vertexBuffer) const
    {
        const std::vector<XMFLOAT3>& sourceVertices = mesh.Vertices();
        std::vector<XMFLOAT3>* textureCoordinates = mesh.TextureCoordinates().at(0);
        assert(textureCoordinates->size() == sourceVertices.size());
        const std::vector<XMFLOAT3>& normals = mesh.Normals();
        assert(textureCoordinates->size() == sourceVertices.size());

        std::vector<DiffuseLightingMaterialVertex> vertices;
        vertices.reserve(sourceVertices.size());
        for (UINT i = 0; i < sourceVertices.size(); i++)
        {
            XMFLOAT3 position = sourceVertices.at(i);
            XMFLOAT3 uv = textureCoordinates->at(i);
            XMFLOAT3 normal = normals.at(i);
            vertices.push_back(DiffuseLightingMaterialVertex(XMFLOAT4(position.x, position.y, position.z, 1.0f), XMFLOAT2(uv.x, uv.y), normal));
        }

        CreateVertexBuffer(device, &vertices[0], vertices.size(), vertexBuffer);
    }

    void DiffuseLightingMaterial::CreateVertexBuffer(ID3D11Device* device, DiffuseLightingMaterialVertex* vertices, UINT vertexCount, ID3D11Buffer** vertexBuffer) const
    {
        D3D11_BUFFER_DESC vertexBufferDesc;
        ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc));
        vertexBufferDesc.ByteWidth = VertexSize() * vertexCount;
        vertexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;		
        vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

        D3D11_SUBRESOURCE_DATA vertexSubResourceData;
        ZeroMemory(&vertexSubResourceData, sizeof(vertexSubResourceData));
        vertexSubResourceData.pSysMem = vertices;
        if (FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexSubResourceData, vertexBuffer)))
        {
            throw GameException("ID3D11Device::CreateBuffer() failed.");
        }
    }

    UINT DiffuseLightingMaterial::VertexSize() const
    {
        return sizeof(DiffuseLightingMaterialVertex);
    }
}


在DiffuseLightingMaterial類的構造函數中,使用MATERIAL_VARIABLE_INITIALIZATION宏對每一個shader variables進行了初始化。然後分別使用MATERIAL_VARIABLE_DEFINITION和MATERIAL_VARIABLE_RETRIVE宏定義了這些變量對應的公有訪問函數以及變量賦值。接下來,在DiffuseLightingMaterial::Initialize()函數中創建了input layout與DiffuseLightingMaterialVertex對應。最後,在DiffuseLightingMaterial::CreateVertexBuffer()函數中,從一個mesh對象中獲取vertex positions,texture coordinates以及normals。這些是具體material的實現細節,不同的material具有不同的實現方式。

A Diffuse Lighting Demo

下面,我們通過一個示例程序中演示如何使用diffuse lighting material。在這個程序中,創建了新的DirectionalLight類,在該類中通過調用DiffuseMaterial的LightColor()和LightDirection()函數給對應的成員賦值。爲了使該示例程序看起來更有趣一點,我們將會使用鍵盤的方向鍵更新光源方向(在運行時),並使用Page Up和Page Down鍵調整ambient light的光照強度。首先,創建一個DiffuseLightingDemo類,該類的聲明代碼如列表17.3所示。
列表17.3 Declaration of the DiffuseLightingDemo Class

class DiffuseLightingDemo : public DrawableGameComponent
{
	RTTI_DECLARATIONS(DiffuseLightingDemo, DrawableGameComponent)

public:
	DiffuseLightingDemo(Game& game, Camera& camera);
	~DiffuseLightingDemo();

	virtual void Initialize() override;
	virtual void Update(const GameTime& gameTime) override;
	virtual void Draw(const GameTime& gameTime) override;

private:
	DiffuseLightingDemo();
	DiffuseLightingDemo(const DiffuseLightingDemo& rhs);
	DiffuseLightingDemo& operator=(const DiffuseLightingDemo& rhs);

	void UpdateAmbientLight(const GameTime& gameTime);
	void UpdateDirectionalLight(const GameTime& gameTime);

	static const float LightModulationRate;
	static const XMFLOAT2 LightRotationRate;

	Effect* mEffect;
	DiffuseLightingMaterial* mMaterial;
	ID3D11ShaderResourceView* mTextureShaderResourceView;
	ID3D11Buffer* mVertexBuffer;
	ID3D11Buffer* mIndexBuffer;
	UINT mIndexCount;

	XMCOLOR mAmbientColor;
	DirectionalLight* mDirectionalLight;
	Keyboard* mKeyboard;
	XMFLOAT4X4 mWorldMatrix;

	ProxyModel* mProxyModel;

	RenderStateHelper* mRenderStateHelper;
	SpriteBatch* mSpriteBatch;
	SpriteFont* mSpriteFont;
	XMFLOAT2 mTextPosition;
};


在DiffuseLightingDemo類中包含了一些常用的數據成員:用於存儲effect和material,用於表示一個texture的shader resource view,用於在場景中變換object的world矩陣,以及vertex和index buffers。新增的成員包括用於存儲ambient color和directional light的變量。其中表示ambient color的成員變量類型爲XMCOLOR類型;理論上應該使用一個通用的Light類對象表示,但是使用一個XMCOLOR類型的變量已經足夠了。另外還有一個新增的ProxyModel數據類型,用於渲染一個表示光源的模型。在實際的遊戲程序中,不應該渲染這種proxy模型,但是在開發過程中是很有用的。在沒有proxy模型的情況下,很難實時觀察一個光源的座標位置和方向。爲了簡潔,在這裏沒有列出ProxyModel類的代碼,而是在圖17.2中展示了類的結構圖。在本書的配套網站上提供了ProxyModel類的完整代碼。


圖17.2 Class diagram for the ProxyModel class.
Diffuse lighting示例中的渲染操作與前面幾個示例中建立的設計方式完全一樣。首先,設置primitive topology和input layout,並把vertex和index buffers綁定到圖形管線的input-assembler階段。然後更新material並執行繪製操作。列表17.4中列出了DiffuseLightingDemo類的Draw()函數代碼。
列表17.4 Implementation of the DiffuseLightingDemo::Draw() Method

void DiffuseLightingDemo::Draw(const GameTime& gameTime)
{
	ID3D11DeviceContext* direct3DDeviceContext = mGame->Direct3DDeviceContext();
	direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

	Pass* pass = mMaterial->CurrentTechnique()->Passes().at(0);
	ID3D11InputLayout* inputLayout = mMaterial->InputLayouts().at(pass);
	direct3DDeviceContext->IASetInputLayout(inputLayout);

	UINT stride = mMaterial->VertexSize();
	UINT offset = 0;
	direct3DDeviceContext->IASetVertexBuffers(0, 1, &mVertexBuffer, &stride, &offset);
	direct3DDeviceContext->IASetIndexBuffer(mIndexBuffer, DXGI_FORMAT_R32_UINT, 0);

	XMMATRIX worldMatrix = XMLoadFloat4x4(&mWorldMatrix);
	XMMATRIX wvp = worldMatrix * mCamera->ViewMatrix() * mCamera->ProjectionMatrix();
	XMVECTOR ambientColor = XMLoadColor(&mAmbientColor);

	mMaterial->WorldViewProjection() << wvp;
	mMaterial->World() << worldMatrix;
	mMaterial->AmbientColor() << ambientColor;
	mMaterial->LightColor() << mDirectionalLight->ColorVector();
	mMaterial->LightDirection() << mDirectionalLight->DirectionVector();
	mMaterial->ColorTexture() << mTextureShaderResourceView;

	pass->Apply(0, direct3DDeviceContext);

	direct3DDeviceContext->DrawIndexed(mIndexCount, 0, 0);

	mProxyModel->Draw(gameTime);

	mRenderStateHelper->SaveAll();
	mSpriteBatch->Begin();

	std::wostringstream helpLabel;
	helpLabel << L"Ambient Intensity (+PgUp/-PgDn): " << mAmbientColor.a << "\n";
	helpLabel << L"Directional Light Intensity (+Home/-End): " << mDirectionalLight->Color().a << "\n";
	helpLabel << L"Rotate Directional Light (Arrow Keys)\n";

	mSpriteFont->DrawString(mSpriteBatch, helpLabel.str().c_str(), mTextPosition);

	mSpriteBatch->End();
	mRenderStateHelper->RestoreAll();
}


圖17.3中顯示了diffuse lighting示例程序的輸出結果。其中proxy model(線框表示的箭頭)描述了光源的照射方向。另外還有一個參考網格,該網絡組件(對應Grid類)用於在示例程序中提供一個參照系。在本書的配套網站上提供了該Grid類的完整代碼。


圖17.3 Output of the diffuse lighting demo. (Original texture from Reto Stöckli, NASA Earth Observatory. Additional texturing by Nick Zuccarello, Florida Interactive Entertainment Academy.)

Interacting with Lights

要在程序運行時動態更新光照是簡單明瞭的。比如,要更新ambient light,只需要修改DiffuseLightingDemo::mAmbientColor變量中的alpha通道值。可以根據一個鍵盤按鍵的響應事件修改該通道值,或者根據一個特定功能的時間而變化(比如創建一個脈衝或閃爍光)。列表17.5列表出一種通過按鍵Page Up和Page Down增加或減少ambient light強度值的方法。這種方法需要在DiffuseLightingDemo::Update()函數中執行。
列表17.5 Incrementing and Decrementing Ambient Light Intensity

void DiffuseLightingDemo::UpdateAmbientLight(const GameTime& gameTime)
{
	static float ambientIntensity = mAmbientColor.a;

	if (mKeyboard != nullptr)
	{
		if (mKeyboard->IsKeyDown(DIK_PGUP) && ambientIntensity < UCHAR_MAX)
		{
			ambientIntensity += LightModulationRate * (float)gameTime.ElapsedGameTime();
			mAmbientColor.a = (UCHAR)XMMin<float>(ambientIntensity, UCHAR_MAX);
		}

		if (mKeyboard->IsKeyDown(DIK_PGDN) && ambientIntensity > 0)
		{
			ambientIntensity -= LightModulationRate * (float)gameTime.ElapsedGameTime();
			mAmbientColor.a = (UCHAR)XMMax<float>(ambientIntensity, 0);
		}
	}
}


在DiffuseLightingDemo::Update()函數中,定義了一個靜態變量ambientIntensity用於實時記錄當前ambient light的強度值,並隨着相關的鍵盤按鍵被按下而更新。另外,變量AmbientModulationRate用於表示ambient的強度值改變的速率。比如,當該變量值 爲255時,ambient強度值可以在1秒內從最小值(0)變化到最大值(255)。
另外,還可以在程序運行時動態改變directinal light。列表17.6提供了一個使用鍵盤方向鍵旋轉一個directional light的函數。
列表17.6 Rotating a Directional Light

void DiffuseLightingDemo::UpdateDirectionalLight(const GameTime& gameTime)
{
	static float directionalIntensity = mDirectionalLight->Color().a;
	float elapsedTime = (float)gameTime.ElapsedGameTime();

	// Update directional light intensity		
	if (mKeyboard->IsKeyDown(DIK_HOME) && directionalIntensity < UCHAR_MAX)
	{
		directionalIntensity += LightModulationRate * elapsedTime;

		XMCOLOR directionalLightColor = mDirectionalLight->Color();
		directionalLightColor.a = (UCHAR)XMMin<float>(directionalIntensity, UCHAR_MAX);
		mDirectionalLight->SetColor(directionalLightColor);
	}
	if (mKeyboard->IsKeyDown(DIK_END) && directionalIntensity > 0)
	{
		directionalIntensity -= LightModulationRate * elapsedTime;

		XMCOLOR directionalLightColor = mDirectionalLight->Color();
		directionalLightColor.a = (UCHAR)XMMax<float>(directionalIntensity, 0.0f);
		mDirectionalLight->SetColor(directionalLightColor);
	}

	// Rotate directional light
	XMFLOAT2 rotationAmount = Vector2Helper::Zero;
	if (mKeyboard->IsKeyDown(DIK_LEFTARROW))
	{
		rotationAmount.x += LightRotationRate.x * elapsedTime;
	}
	if (mKeyboard->IsKeyDown(DIK_RIGHTARROW))
	{
		rotationAmount.x -= LightRotationRate.x * elapsedTime;
	}
	if (mKeyboard->IsKeyDown(DIK_UPARROW))
	{
		rotationAmount.y += LightRotationRate.y * elapsedTime;
	}
	if (mKeyboard->IsKeyDown(DIK_DOWNARROW))
	{
		rotationAmount.y -= LightRotationRate.y * elapsedTime;
	}

	XMMATRIX lightRotationMatrix = XMMatrixIdentity();
	if (rotationAmount.x != 0)
	{
		lightRotationMatrix = XMMatrixRotationY(rotationAmount.x);
	}

	if (rotationAmount.y != 0)
	{
		XMMATRIX lightRotationAxisMatrix = XMMatrixRotationAxis(mDirectionalLight->RightVector(), rotationAmount.y);
		lightRotationMatrix *= lightRotationAxisMatrix;
	}

	if (rotationAmount.x != 0.0f || rotationAmount.y != 0.0f)
	{
		mDirectionalLight->ApplyRotation(lightRotationMatrix);
		mProxyModel->ApplyRotation(lightRotationMatrix);
	}
}


在該函數中,LightRotationRate向量表示light旋轉的速率(向量中兩個分量值可以針對水平和垂直方向使用不同的旋轉速度)。使用該向量計算得到的旋轉數量值rotationAmount,創建一個繞y軸旋轉的矩陣以及一個繞directinal light的right向量旋轉的矩陣。最後使用這些旋轉矩陣(很可能同時由水平和垂直兩個方向的旋轉矩陣組)對directional light以及proxy模型執行變換。

發佈了5 篇原創文章 · 獲贊 27 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章