DirectX11 With Windows SDK--13 動手實現一個簡易Effects框架、陰影效果繪制


前言

到現在為止,所有的教程項目都沒有使用Effects11框架類來管理資源。因為在D3DCompile API (#47)版本中,如果你嘗試編譯fx_5_0的效果文件,會收到這樣的警告:
X4717: Effects deprecated for D3DCompiler_47

在未來的版本中,D3DCompiler可能會停止對FX11的支持,所以我們需要自行去管理各種特效,並改用HLSL編譯器去編譯每一個着色器。同時,在閱讀本章之前,你需要先學習本系列前面的一些重點章節再繼續:

章節目錄
01 DirectX11初始化
02 頂點/像素着色器的創建、頂點緩沖區
03 索引緩沖區、常量緩沖區
09 紋理映射與采樣器狀態
11 混合狀態與光柵化狀態
12 深度/模板狀態、反射繪制

在DirectXTK中的Effects.h可以看到它實現了一系列Effects管理類,相比Effects11框架庫,它缺少了反射機制,並且使用的是它內部已經寫好、編譯好的着色器。DirectXTK的Effects也只不過是為了簡化游戲開發流程而設計出來的。當然,里面的一部分源碼實現也值得我們去學習。

注意:這章經歷了一次十分大的改動,原先所使用的BasicEffect類因為在后續的章節中發現很難擴展,所以進行了一次大幅度重構。並會逐漸替換掉后面教程的項目源碼所使用的BasicEffect。

在這一章的學習過后,你將會理解Effects11的一部分運作機制是怎樣的。如果想更深入了解的話,推薦閱讀下面這篇,內部實現了一個功能和Effects11相仿的EffectHelper類,可以更好地幫助你簡化代碼:

章節目錄
深入理解Effects11、使用着色器反射機制(Shader Reflection)實現一個復雜Effects框架

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。

回顧RenderStates類

目前的RenderStates類存放有比較常用的各種狀態,原來在Effects11框架下是可以在fx文件初始化各種渲染狀態,並設置到Technique11中。但現在我們只能在C++代碼層中一次性創建好各種所需的渲染狀態:

class RenderStates
{
public:
	template <class T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	static bool IsInit();

	static void InitAll(ID3D11Device * device);
	// 使用ComPtr無需手工釋放

public:
	static ComPtr<ID3D11RasterizerState> RSWireframe;		    // 光柵化器狀態:線框模式
	static ComPtr<ID3D11RasterizerState> RSNoCull;			    // 光柵化器狀態:無背面裁剪模式
	static ComPtr<ID3D11RasterizerState> RSCullClockWise;	    // 光柵化器狀態:順時針裁剪模式

	static ComPtr<ID3D11SamplerState> SSLinearWrap;			    // 采樣器狀態:線性過濾
	static ComPtr<ID3D11SamplerState> SSAnistropicWrap;		    // 采樣器狀態:各項異性過濾

	static ComPtr<ID3D11BlendState> BSNoColorWrite;		        // 混合狀態:不寫入顏色
	static ComPtr<ID3D11BlendState> BSTransparent;		        // 混合狀態:透明混合
	static ComPtr<ID3D11BlendState> BSAlphaToCoverage;	        // 混合狀態:Alpha-To-Coverage

	static ComPtr<ID3D11DepthStencilState> DSSWriteStencil;		// 深度/模板狀態:寫入模板值
	static ComPtr<ID3D11DepthStencilState> DSSDrawWithStencil;	// 深度/模板狀態:對指定模板值的區域進行繪制
	static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend;	// 深度/模板狀態:無二次混合區域
	static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest;		// 深度/模板狀態:關閉深度測試
	static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite;		// 深度/模板狀態:僅深度測試,不寫入深度值
};

具體的設置可以參照源碼或者上一章內容。

簡易Effects框架

該Effects框架支持的功能如下:

  1. 管理/修改常量緩沖區的內容,並應用(Apply)變更
  2. 編譯HLSL着色器而不是fx文件
  3. 管理/使用四種渲染狀態
  4. 切換渲染模式(涉及到渲染管線各種資源的綁定、切換)
  5. 僅更新修改的變量所對應的常量緩沖區塊

不過它也有這樣的缺陷:

  1. 一個特效類對應一套着色器和所使用的常量緩沖區,所屬着色器代碼的變動很可能會引起對框架類的修改,因為缺乏反射機制而導致靈活性差。

此外,該框架內部會對矩陣進行轉置,因此在傳遞矩陣給Effects時只需要傳遞默認的行主矩陣即可。

文件結構

首先是文件結構:

其中能夠暴露給程序使用的只有頭文件Effects.h,里面可以存放多套不同的特效框架類的聲明,而關於每個框架類的實現部分都應當用一個獨立的源文件存放。而EffectHelper.h則是用來幫助管理常量緩沖區的,服務於各種框架類的實現部分以及所屬的源文件,因此不應該直接使用。

理論上它也是可以做成靜態庫使用的,然后着色器代碼穩定后也不應當變動。在使用的時候只需要包含頭文件Effects.h即可。

EffectHelper.h

該頭文件包含了一些有用的東西,但它需要在包含特效類實現的源文件中使用,且必須晚於Effects.hd3dUtil.h包含。

在堆上進行類的內存對齊

有些類型需要在堆上按16字節對齊,比如XMVECTORXMMATRIX,雖然說拿這些對象作為類的成員不太合適,畢竟分配在堆上的話基本上無法保證內存按16字節對齊了,但還是希望能夠做到。在VS的corecrt_malloc.h(只要有包含stdlib.h, malloc.h之一的頭文件都可以)中有這樣的一個函數:_aligned_malloc,它可以指定需要分配的內存字節大小以及按多少字節對齊。其中對齊值必須為2的整數次冪的字節數。

void * _aligned_malloc(  
    size_t size,        // [In]分配內存字節數
    size_t alignment    // [In]按多少字節內存來對齊
);  

若一個類中包含有已經指定內存對齊的成員,則需要優先把這些成員放到最前。

然后與之對應的就是_aligned_free函數了,它可以釋放之前由_aligned_malloc分配得到的內存。

下面是類模板AlignedType的實現,讓需要內存對齊的類去繼承該類即可。它重載了operator newoperator delete的實現:

// 若類需要內存對齊,從該類派生
template<class DerivedType>
struct AlignedType
{
	static void* operator new(size_t size)
	{
		const size_t alignedSize = __alignof(DerivedType);

		static_assert(alignedSize > 8, "AlignedNew is only useful for types with > 8 byte alignment! Did you forget a __declspec(align) on DerivedType?");

		void* ptr = _aligned_malloc(size, alignedSize);

		if (!ptr)
			throw std::bad_alloc();

		return ptr;
	}

	static void operator delete(void * ptr)
	{
		_aligned_free(ptr);
	}
};

需要注意的是,繼承AlignedType的類或者其成員必須本身有__declspec(align)的標識。若是內部成員,在所有包含該標識的值中最大的align值 必須是2的整數次冪且必須大於8。

下面演示了正確的和錯誤的行為:

// 錯誤!VertexPosColor按4字節對齊!
struct VertexPosColor : AlignedType<VertexPos>
{
    XMFLOAT3 pos;
    XMFLOAT4 color;
};

// 正確!Data按16字節對齊,因為pos本身是按16字節對齊的。
struct Data : AlignedType<VertexPos>
{
    XMVECTOR pos;
    int val;
};

// 正確!Vector類按16字節對齊
__declspec(align(16))
struct Vector : AlignedType<Vector>
{
	float x;
	float y;
	float z;
	float w;
};

這里AlignedType<T>主要是用於BasicEffect::Impl類,因為其內部包含了XMVECTORXMMATRIX類型的成員,且該類需要分配在堆上。

常量緩沖區管理

一個常量緩沖區可能會被創建、更新或者綁定到管線。若常量緩沖區的值沒有發生變化,我們不希望它進行無意義的更新。這里可以使用一個dirty標記,確認它是否被修改過。在Effects調用Apply后,如果常量緩沖區的任一內部成員發生修改的話,我們就將數據更新到常量緩沖區並恢復該標記。

首先是抽象基類CBufferBase

struct CBufferBase
{
	template<class T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	CBufferBase() : isDirty() {}
	~CBufferBase() = default;

	BOOL isDirty;
	ComPtr<ID3D11Buffer> cBuffer;

	virtual HRESULT CreateBuffer(ID3D11Device * device) = 0;
	virtual void UpdateBuffer(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindVS(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindHS(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindDS(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindGS(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindCS(ID3D11DeviceContext * deviceContext) = 0;
	virtual void BindPS(ID3D11DeviceContext * deviceContext) = 0;
};

這么做是為了方便我們放入數組進行遍歷。

然后是派生類CBufferObjectstartSlot指定了HLSL對應cbuffer的索引,T則是C++對應的結構體,存儲臨時數據:

template<UINT startSlot, class T>
struct CBufferObject : CBufferBase
{
	T data;

	CBufferObject() : CBufferBase(), data() {}

	HRESULT CreateBuffer(ID3D11Device * device) override
	{
		if (cBuffer != nullptr)
			return S_OK;
		D3D11_BUFFER_DESC cbd;
		ZeroMemory(&cbd, sizeof(cbd));
		cbd.Usage = D3D11_USAGE_DYNAMIC;
		cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
		cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
		cbd.ByteWidth = sizeof(T);
		return device->CreateBuffer(&cbd, nullptr, cBuffer.GetAddressOf());
	}

	void UpdateBuffer(ID3D11DeviceContext * deviceContext) override
	{
		if (isDirty)
		{
			isDirty = false;
			D3D11_MAPPED_SUBRESOURCE mappedData;
			deviceContext->Map(cBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
			memcpy_s(mappedData.pData, sizeof(T), &data, sizeof(T));
			deviceContext->Unmap(cBuffer.Get(), 0);
		}
	}

	void BindVS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->VSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}

	void BindHS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->HSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}

	void BindDS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->DSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}

	void BindGS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->GSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}

	void BindCS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->CSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}

	void BindPS(ID3D11DeviceContext * deviceContext) override
	{
		deviceContext->PSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
	}
};

關於常量緩沖區臨時變量的修改則在后續的內容。

BasicEffect類--管理對象繪制的資源

首先是抽象基類IEffects,它僅允許被移動,並且僅包含Apply方法。

class IEffect
{
public:
	// 使用模板別名(C++11)簡化類型名
	template <class T>
	using ComPtr = Microsoft::WRL::ComPtr<T>;

	IEffect() = default;

	// 不支持復制構造
	IEffect(const IEffect&) = delete;
	IEffect& operator=(const IEffect&) = delete;

	// 允許轉移
	IEffect(IEffect&& moveFrom) = default;
	IEffect& operator=(IEffect&& moveFrom) = default;

	virtual ~IEffect() = default;

	// 更新並綁定常量緩沖區
	virtual void Apply(ID3D11DeviceContext * deviceContext) = 0;
};

原來的ID3DX11EffectPass包含的方法Apply用於在各個着色器階段綁定所需要的常量緩沖區、紋理等資源,並更新之前有所修改的常量緩沖區。現在我們實現Effects框架中的Apply方法也是這么做的。

然后是派生類BasicEffect,從它的方法來看,包含了單例獲取、渲染狀態的切換、修改常量緩沖區某一成員的值、應用變更四個大塊:

class BasicEffect : public IEffect
{
public:

	BasicEffect();
	virtual ~BasicEffect() override;

	BasicEffect(BasicEffect&& moveFrom) noexcept;
	BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;

	// 獲取單例
	static BasicEffect& Get();

	

	// 初始化Basic.hlsli所需資源並初始化渲染狀態
	bool InitAll(ID3D11Device * device);


	//
	// 渲染模式的變更
	//

	// 默認狀態來繪制
	void SetRenderDefault(ID3D11DeviceContext * deviceContext);
	// Alpha混合繪制
	void SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
	// 無二次混合
	void SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef);
	// 僅寫入模板值
	void SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef);
	// 對指定模板值的區域進行繪制,采用默認狀態
	void SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
	// 對指定模板值的區域進行繪制,采用Alpha混合
	void SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
	// 2D默認狀態繪制
	void Set2DRenderDefault(ID3D11DeviceContext * deviceContext);
	// 2D混合繪制
	void Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext);

	

	//
	// 矩陣設置
	//

	void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);
	void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);
	void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);

	void XM_CALLCONV SetReflectionMatrix(DirectX::FXMMATRIX R);
	void XM_CALLCONV SetShadowMatrix(DirectX::FXMMATRIX S);
	void XM_CALLCONV SetRefShadowMatrix(DirectX::FXMMATRIX RefS);
	
	//
	// 光照、材質和紋理相關設置
	//

	// 各種類型燈光允許的最大數目
	static const int maxLights = 5;

	void SetDirLight(size_t pos, const DirectionalLight& dirLight);
	void SetPointLight(size_t pos, const PointLight& pointLight);
	void SetSpotLight(size_t pos, const SpotLight& spotLight);

	void SetMaterial(const Material& material);

	void SetTexture(ID3D11ShaderResourceView * texture);

	void SetEyePos(const DirectX::XMFLOAT3& eyePos);



	//
	// 狀態開關設置
	//

	void SetReflectionState(bool isOn);
	void SetShadowState(bool isOn);
	

	// 應用常量緩沖區和紋理資源的變更
	void Apply(ID3D11DeviceContext * deviceContext);
	
private:
	class Impl;
	std::unique_ptr<Impl> pImpl;
};

XM_CALLCONV即在第五章之前提到的__vectorcall__fastcall約定。

BasicEffect::Impl類

之前在BasicEffect中聲明了Impl類,主要目的是為了將類的成員和方法定義都轉移到源文件中,並且還包含了HLSL五個cbuffer的C++結構體。不僅可以減少BasicEffect類的壓力,還可以避免暴露上面的五個結構體:

class BasicEffect::Impl : public AlignedType<BasicEffect::Impl>
{
public:

	//
	// 這些結構體對應HLSL的結構體。需要按16字節對齊
	//

	struct CBChangesEveryDrawing
	{
		DirectX::XMMATRIX world;
		DirectX::XMMATRIX worldInvTranspose;
		Material material;
	};

	struct CBDrawingStates
	{
		int isReflection;
		int isShadow;
		DirectX::XMINT2 pad;
	};

	struct CBChangesEveryFrame
	{
		DirectX::XMMATRIX view;
		DirectX::XMFLOAT3 eyePos;
		float pad;
	};

	struct CBChangesOnResize
	{
		DirectX::XMMATRIX proj;
	};


	struct CBChangesRarely
	{
		DirectX::XMMATRIX reflection;
		DirectX::XMMATRIX shadow;
		DirectX::XMMATRIX refShadow;
		DirectionalLight dirLight[BasicEffect::maxLights];
		PointLight pointLight[BasicEffect::maxLights];
		SpotLight spotLight[BasicEffect::maxLights];
	};

public:
	// 必須顯式指定
	Impl() : m_IsDirty() {}
	~Impl() = default;

public:
	// 需要16字節對齊的優先放在前面
	CBufferObject<0, CBChangesEveryDrawing> m_CBDrawing;		// 每次對象繪制的常量緩沖區
	CBufferObject<1, CBDrawingStates>       m_CBStates;		    // 每次繪制狀態變更的常量緩沖區
	CBufferObject<2, CBChangesEveryFrame>   m_CBFrame;		    // 每幀繪制的常量緩沖區
	CBufferObject<3, CBChangesOnResize>     m_CBOnResize;		// 每次窗口大小變更的常量緩沖區
	CBufferObject<4, CBChangesRarely>		m_CBRarely;		    // 幾乎不會變更的常量緩沖區
	BOOL m_IsDirty;												// 是否有值變更
	std::vector<CBufferBase*> m_pCBuffers;					    // 統一管理上面所有的常量緩沖區


	ComPtr<ID3D11VertexShader> m_pVertexShader3D;				// 用於3D的頂點着色器
	ComPtr<ID3D11PixelShader>  m_pPixelShader3D;				// 用於3D的像素着色器
	ComPtr<ID3D11VertexShader> m_pVertexShader2D;				// 用於2D的頂點着色器
	ComPtr<ID3D11PixelShader>  m_pPixelShader2D;				// 用於2D的像素着色器

	ComPtr<ID3D11InputLayout>  m_pVertexLayout2D;				// 用於2D的頂點輸入布局
	ComPtr<ID3D11InputLayout>  m_pVertexLayout3D;				// 用於3D的頂點輸入布局

	ComPtr<ID3D11ShaderResourceView> m_pTexture;				// 用於繪制的紋理

};

構造/析構/單例

這里用一個匿名空間保管單例對象的指針。當有一個實例被構造出來的時候就會給其賦值。后續就不允許再被實例化了,可以使用Get方法獲取該單例。

namespace
{
	// BasicEffect單例
	static BasicEffect * g_pInstance = nullptr;
}

BasicEffect::BasicEffect()
{
	if (g_pInstance)
		throw std::exception("BasicEffect is a singleton!");
	g_pInstance = this;
	pImpl = std::make_unique<BasicEffect::Impl>();
}

BasicEffect::~BasicEffect()
{
}

BasicEffect::BasicEffect(BasicEffect && moveFrom) noexcept
{
	pImpl.swap(moveFrom.pImpl);
}

BasicEffect & BasicEffect::operator=(BasicEffect && moveFrom) noexcept
{
	pImpl.swap(moveFrom.pImpl);
	return *this;
}

BasicEffect & BasicEffect::Get()
{
	if (!g_pInstance)
		throw std::exception("BasicEffect needs an instance!");
	return *g_pInstance;
}

BasicEffect::InitAll方法

BasicEffect::InitAll方法負責創建出所有的着色器和常量緩沖區,以及所有的渲染狀態:

bool BasicEffect::InitAll(ID3D11Device * device)
{
	if (!device)
		return false;

	if (!pImpl->m_pCBuffers.empty())
		return true;

	if (!RenderStates::IsInit())
		throw std::exception("RenderStates need to be initialized first!");

	ComPtr<ID3DBlob> blob;

	// 創建頂點着色器(2D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.GetAddressOf()));
	HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader2D.GetAddressOf()));
	// 創建頂點布局(2D)
	HR(device->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
		blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout2D.GetAddressOf()));

	// 創建像素着色器(2D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
	HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader2D.GetAddressOf()));

	// 創建頂點着色器(3D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
	HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader3D.GetAddressOf()));
	// 創建頂點布局(3D)
	HR(device->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
		blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout3D.GetAddressOf()));

	// 創建像素着色器(3D)
	HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
	HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader3D.GetAddressOf()));


	pImpl->m_pCBuffers.assign({
		&pImpl->m_CBDrawing, 
		&pImpl->m_CBFrame, 
		&pImpl->m_CBStates, 
		&pImpl->m_CBOnResize, 
		&pImpl->m_CBRarely});

	// 創建常量緩沖區
	for (auto& pBuffer : pImpl->m_pCBuffers)
	{
		HR(pBuffer->CreateBuffer(device));
	}

	return true;
}

各種渲染狀態的切換

下面所有的渲染模式使用的是線性Wrap采樣器。

BasicEffect::SetRenderDefault方法--默認渲染

BasicEffect::SetRenderDefault方法使用了默認的3D像素着色器和頂點着色器,並且其余各狀態都保留使用默認狀態:

void BasicEffect::SetRenderDefault(ID3D11DeviceContext * deviceContext)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(nullptr);
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(nullptr, 0);
	deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderAlphaBlend方法--Alpha透明混合渲染

該繪制模式關閉了光柵化裁剪,並采用透明混合方式。

void BasicEffect::SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSNoCull.Get());
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(nullptr, 0);
	deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderNoDoubleBlend方法--無重復混合(單次混合)

該繪制模式用於繪制陰影,防止過度混合。需要指定繪制區域的模板值。

void BasicEffect::SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSNoCull.Get());
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDoubleBlend.Get(), stencilRef);
	deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetWriteStencilOnly方法--僅寫入模板值

該模式用於向模板緩沖區寫入用戶指定的模板值,並且不寫入到深度緩沖區和后備緩沖區。

void BasicEffect::SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(nullptr);
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), stencilRef);
	deviceContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderDefaultWithStencil方法--對指定模板值區域進行常規繪制

該模式下,僅對模板緩沖區的模板值和用戶指定的相等的區域進行常規繪制。

void BasicEffect::SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSCullClockWise.Get());
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
	deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderAlphaBlendWithStencil方法--對指定模板值區域進行Alpha透明混合繪制

該模式下,僅對模板緩沖區的模板值和用戶指定的相等的區域進行Alpha透明混合繪制。

void BasicEffect::SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSNoCull.Get());
	deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
	deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::Set2DRenderDefault方法--2D默認繪制

該模式使用的是2D頂點着色器和像素着色器,並修改為2D輸入布局。

void BasicEffect::Set2DRenderDefault(ID3D11DeviceContext * deviceContext)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
	deviceContext->RSSetState(nullptr);
	deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(nullptr, 0);
	deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::Set2DRenderAlphaBlend方法--2D透明混合繪制

相比上面,多了透明混合狀態。

void BasicEffect::Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
	deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
	deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
	deviceContext->RSSetState(RenderStates::RSNoCull.Get());
	deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
	deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
	deviceContext->OMSetDepthStencilState(nullptr, 0);
	deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

更新常量緩沖區

下面這些所有的方法會更新CBufferObject中的臨時數據,數據臟標記被設為true

void XM_CALLCONV BasicEffect::SetWorldMatrix(DirectX::FXMMATRIX W)
{
	auto& cBuffer = pImpl->m_CBDrawing;
	cBuffer.data.world = XMMatrixTranspose(W);
	cBuffer.data.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetViewMatrix(FXMMATRIX V)
{
	auto& cBuffer = pImpl->m_CBFrame;
	cBuffer.data.view = XMMatrixTranspose(V);
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetProjMatrix(FXMMATRIX P)
{
	auto& cBuffer = pImpl->m_CBOnResize;
	cBuffer.data.proj = XMMatrixTranspose(P);
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetReflectionMatrix(FXMMATRIX R)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.reflection = XMMatrixTranspose(R);
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetShadowMatrix(FXMMATRIX S)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.shadow = XMMatrixTranspose(S);
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetRefShadowMatrix(DirectX::FXMMATRIX RefS)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.refShadow = XMMatrixTranspose(RefS);
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetDirLight(size_t pos, const DirectionalLight & dirLight)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.dirLight[pos] = dirLight;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetPointLight(size_t pos, const PointLight & pointLight)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.pointLight[pos] = pointLight;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetSpotLight(size_t pos, const SpotLight & spotLight)
{
	auto& cBuffer = pImpl->m_CBRarely;
	cBuffer.data.spotLight[pos] = spotLight;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetMaterial(const Material & material)
{
	auto& cBuffer = pImpl->m_CBDrawing;
	cBuffer.data.material = material;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetTexture(ID3D11ShaderResourceView * m_pTexture)
{
	pImpl->m_pTexture = m_pTexture;
}

void BasicEffect::SetEyePos(const XMFLOAT3& eyePos)
{
	auto& cBuffer = pImpl->m_CBFrame;
	cBuffer.data.eyePos = eyePos;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetReflectionState(bool isOn)
{
	auto& cBuffer = pImpl->m_CBStates;
	cBuffer.data.isReflection = isOn;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetShadowState(bool isOn)
{
	auto& cBuffer = pImpl->m_CBStates;
	cBuffer.data.isShadow = isOn;
	pImpl->m_IsDirty = cBuffer.isDirty = true;
}

BasicEffect::Apply方法--應用緩沖區、紋理資源並進行更新

BasicEffect::Apply首先將所需要用到的緩沖區綁定到渲染管線上,並設置紋理,然后才是視情況更新常量緩沖區。

下面的緩沖區數組索引值同時也對應了之前編譯期指定的startSlot值。

首先檢驗總的臟標記是否為true,若有任意數據被修改,則檢驗每個常量緩沖區的臟標記,並根據該標記決定是否要更新常量緩沖區。

void BasicEffect::Apply(ID3D11DeviceContext * deviceContext)
{
	auto& pCBuffers = pImpl->m_pCBuffers;
	// 將緩沖區綁定到渲染管線上
	pCBuffers[0]->BindVS(deviceContext);
	pCBuffers[1]->BindVS(deviceContext);
	pCBuffers[2]->BindVS(deviceContext);
	pCBuffers[3]->BindVS(deviceContext);
	pCBuffers[4]->BindVS(deviceContext);

	pCBuffers[0]->BindPS(deviceContext);
	pCBuffers[1]->BindPS(deviceContext);
	pCBuffers[2]->BindPS(deviceContext);
	pCBuffers[4]->BindPS(deviceContext);

	// 設置紋理
	deviceContext->PSSetShaderResources(0, 1, pImpl->m_pTexture.GetAddressOf());

	if (pImpl->m_IsDirty)
	{
		pImpl->m_IsDirty = false;
		for (auto& pCBuffer : pCBuffers)
		{
			pCBuffer->UpdateBuffer(deviceContext);
		}
	}
}

當然,目前BasicEffect能做的事情還是比較有限的,並且還需要隨着HLSL代碼的變動而隨之調整。更多的功能會在后續教程中實現。

繪制平面陰影

使用XMMatrixShadow可以生成陰影矩陣,根據光照類型和位置對幾何體投影到平面上的。

XMMATRIX XMMatrixShadow(
	FXMVECTOR ShadowPlane,		// 平面向量(nx, ny, nz, d)
	FXMVECTOR LightPosition);	// w = 0時表示平行光方向, w = 1時表示光源位置

通常指定的平面會稍微比實際平面高那么一點點,以避免深度緩沖區資源爭奪導致陰影顯示有問題。

使用模板緩沖區防止過度混合

一個物體投影到平面上時,投影區域的某些位置可能位於多個三角形之內,這會導致這些位置會有多個像素通過測試並進行混合操作,渲染的次數越多,顯示的顏色會越黑。

我們可以使用模板緩沖區來解決這個問題。

  1. 在之前的例子中,我們用模板值為0的區域表示非鏡面反射區,模板值為1的區域表示為鏡面反射區;
  2. 使用RenderStates::DSSNoDoubleBlend的深度模板狀態,當給定的模板值和深度/模板緩沖區的模板值一致時,通過模板測試並對模板值加1,繪制該像素的混合,然后下一次由於給定的模板值比深度/模板緩沖區的模板值小1,不會再通過模板測試,也就阻擋了后續像素的繪制;
  3. 應當先繪制鏡面的陰影區域,再繪制正常的陰影區域。

着色器代碼的變化

Basic_PS_2D.hlsl文件變化如下:

#include "Basic.hlsli"

// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
    float4 color = g_Tex.Sample(g_Sam, pIn.Tex);
    clip(color.a - 0.1f);
    return color;
}

Basic_PS_3D.hlsl文件變化如下:

#include "Basic.hlsli"

// 像素着色器(3D)
// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
	// 提前進行裁剪,對不符合要求的像素可以避免后續運算
    float4 texColor = g_Tex.Sample(g_Sam, pIn.Tex);
    clip(texColor.a - 0.1f);

    // 標准化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 頂點指向眼睛的向量
    float3 toEyeW = normalize(g_EyePosW - pIn.PosW);

    // 初始化為0 
    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 = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
    int i;


    [unroll]
    for (i = 0; i < 5; ++i)
    {
        DirectionalLight dirLight = g_DirLight[i];
        [flatten]
        if (g_IsReflection)
        {
            dirLight.Direction = mul(dirLight.Direction, (float3x3) (g_Reflection));
        }
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
        
    

    
    // 若當前在繪制反射物體,需要對光照進行反射矩陣變換
    PointLight pointLight;
    [unroll]
    for (i = 0; i < 5; ++i)
    {
        pointLight = g_PointLight[i];
        [flatten]
        if (g_IsReflection)
        {
            pointLight.Position = (float3) mul(float4(pointLight.Position, 1.0f), g_Reflection);
        }
        ComputePointLight(g_Material, pointLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
        
    
	
    SpotLight spotLight; 
    // 若當前在繪制反射物體,需要對光照進行反射矩陣變換
    [unroll]
    for (i = 0; i < 5; ++i)
    {
        spotLight = g_SpotLight[i];
        [flatten]
        if (g_IsReflection)
        {
            spotLight.Position = (float3) mul(float4(spotLight.Position, 1.0f), g_Reflection);
            spotLight.Direction = mul(spotLight.Direction, (float3x3) g_Reflection);
        }
        ComputeSpotLight(g_Material, spotLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
        
    

	
    float4 litColor = texColor * (ambient + diffuse) + spec;
    litColor.a = texColor.a * g_Material.Diffuse.a;
    return litColor;
}

Basic_VS_2D.hlsl變化如下:

#include "Basic.hlsli"

// 頂點着色器(2D)
VertexPosHTex VS_2D(VertexPosTex vIn)
{
    VertexPosHTex vOut;
    vOut.PosH = float4(vIn.PosL, 1.0f);
    vOut.Tex = vIn.Tex;
    return vOut;
}


Basic_VS_3D.hlsl變化如下:

#include "Basic.hlsli"

// 頂點着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    
    matrix viewProj = mul(g_View, g_Proj);
    float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
    float3 normalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
    // 若當前在繪制反射物體,先進行反射操作
    [flatten]
    if (g_IsReflection)
    {
        posW = mul(posW, g_Reflection);
        normalW = mul(normalW, (float3x3) g_Reflection);
    }
    // 若當前在繪制陰影,先進行投影操作
    [flatten]
    if (g_IsShadow)
    {
        posW = (g_IsReflection ? mul(posW, g_RefShadow) : mul(posW, g_Shadow));
    }

    vOut.PosH = mul(posW, viewProj);
    vOut.PosW = posW.xyz;
    vOut.NormalW = normalW;
    vOut.Tex = vIn.Tex;
    return vOut;
}

GameObject類與BasicEffect類的對接

由於GameObject類也承擔了繪制方法,那么最后的Apply也需要交給游戲對象來調用。因此GameObject::Draw方法變更如下:

void GameObject::Draw(ID3D11DeviceContext * deviceContext, BasicEffect& effect)
{
	// 設置頂點/索引緩沖區
	UINT strides = m_VertexStride;
	UINT offsets = 0;
	deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &strides, &offsets);
	deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

	// 更新數據並應用
	effect.SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM());
	effect.SetTexture(m_pTexture.Get());
	effect.SetMaterial(m_Material);
	effect.Apply(deviceContext);

	deviceContext->DrawIndexed(m_IndexCount, 0, 0);
}

場景繪制

現在場景只有牆體、地板、木箱和鏡面。

第1步: 鏡面區域寫入模板緩沖區

// *********************
// 1. 給鏡面反射區域寫入值1到模板緩沖區
// 

m_BasicEffect.SetWriteStencilOnly(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

第2步: 繪制不透明的反射物體

// ***********************
// 2. 繪制不透明的反射物體
//

// 開啟反射繪制
m_BasicEffect.SetReflectionState(true);
m_BasicEffect.SetRenderDefaultWithStencil(m_pd3dImmediateContext.Get(), 1);

m_Walls[2].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[3].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[4].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

第3步: 繪制不透明反射物體的陰影

// ***********************
// 3. 繪制不透明反射物體的陰影
//

m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true);	// 反射開啟,陰影開啟			
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 1);

m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

// 恢復到原來的狀態
m_BasicEffect.SetShadowState(false);
m_WoodCrate.SetMaterial(m_WoodCrateMat);

第4步: 繪制透明鏡面

// ***********************
// 4. 繪制透明鏡面
//

// 關閉反射繪制
m_BasicEffect.SetReflectionState(false);
m_BasicEffect.SetRenderAlphaBlendWithStencil(m_pd3dImmediateContext.Get(), 1);

m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

第5步:繪制不透明的正常物體

// ************************
// 5. 繪制不透明的正常物體
//
m_BasicEffect.SetRenderDefault(m_pd3dImmediateContext.Get());

for (auto& wall : m_Walls)
	wall.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

第6步:繪制不透明正常物體的陰影

// ************************
// 6. 繪制不透明正常物體的陰影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true);	// 反射關閉,陰影開啟
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 0);

m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

m_BasicEffect.SetShadowState(false);		// 陰影關閉
m_WoodCrate.SetMaterial(m_WoodCrateMat);

最終繪制效果如下:

注意該樣例只生成點光燈到地板的陰影。你可以用各種攝像機模式來進行測試。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM