第十七章 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模型执行变换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值