DirectX11 With Windows SDK--35 粒子系統

前言

在這一章中,咱們主要關注的是如何模擬一系列粒子,並控制它們運動。這些粒子的行爲都是相似的,但它們也帶有必定的隨機性。這一堆粒子的幾何咱們叫它爲粒子系統,它能夠被用於模擬一些比較現象,如:火焰、雨、煙霧、爆炸、法術效果等。html

在這一章開始以前,你須要先學過以下章節:git

章節
11 混合狀態
15 幾何着色器初探
16 流輸出階段
17 利用幾何着色器實現公告板效果

學習目標github

  1. 熟悉如何利用幾何着色器和流輸出階段來高效存儲、渲染粒子
  2. 瞭解咱們如何利用基本的物理概念來讓咱們的粒子可以以物理上的真實方式來運動
  3. 設計一個靈活的粒子系統框架使得咱們能夠方便地建立新的自定義粒子系統

DirectX11 With Windows SDK完整目錄數組

Github項目源碼框架

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。dom

粒子的表示

粒子是一種很是小的對象,一般在數學上能夠表示爲一個點。所以咱們在D3D中能夠考慮使用圖元類型D3D11_PRIMITIVE_TOPOLOGY_POINTLIST來將一系列點傳入。然而,點圖元僅僅會被光柵化爲單個像素。考慮到粒子能夠有不一樣的大小,而且甚至須要將整個紋理都貼到這些粒子上,咱們將採用前面公告板的繪製策略,即在頂點着色器輸出頂點,而後在幾何着色器將其變成一個四邊形並朝向攝像機。此外須要注意的是,這些粒子的y軸是與攝像機的y軸是對齊的。ide

若是咱們知道世界座標系的上方向軸向量j,公告板中心位置C,以及攝像機位置E,這樣咱們能夠描述公告板在世界座標系下的局部座標軸(即粒子的世界變換)是怎樣的:函數

\[\mathbf{w}=\frac{\mathbf{E-C}}{\|\mathbf{E-C}\|}\\ \mathbf{u}=\frac{\mathbf{j\times w}}{\|\mathbf{j\times w}\|}\\ \mathbf{v}=\mathbf{w\times u}\\ \mathbf{W}=\begin{bmatrix} u_x & u_y & u_z & 0\\ v_x & v_y & v_z & 0\\ w_x & w_y & w_z & 0\\ C_x & C_y & C_z & 1\\ \end{bmatrix} \]

粒子的屬性以下:學習

struct Particle
{
    XMFLOAT3 InitialPos;
    XMFLOAT3 InitialVel;
    XMFLOAT2 Size;
    float Age;
    unsigned int Type;
};

注意:咱們不須要將頂點變成四邊形。例如,使用LineList來渲染雨看起來工做的也挺不錯,但而咱們能夠用不一樣的幾何着色器來將點變成線。一般狀況下,在咱們的系統中,每一個粒子系統擁有本身的一套特效和着色器集合。測試

粒子運動

咱們將會讓粒子以物理上的真實方式進行運動。爲了簡便,咱們將限制粒子的加速度爲恆定常數。例如,讓加速度取決於重力,又或者是純粹的風力。此外,咱們不對粒子作任何的碰撞檢測。

p(t)爲粒子在t時刻的位置,它的運動軌跡爲一條光滑曲線。它在t時刻的瞬時速度爲:

\[\mathbf{v}(t)=\mathbf{p'}(t) \]

一樣,粒子在t時刻的瞬時加速度爲:

\[\mathbf{a}(t)=\mathbf{v'}(t)=\mathbf{p''}(t) \]

學太高數的話下面的公式不難理解:

\[\int\mathbf{f(t)}dt=\mathbf{F}(t) + C\\ [\mathbf{F}(t)+C]'=\mathbf{f}(t) \]

連續函數f(t)的不定積分獲得的函數有無限個,即C能夠爲任意常數,這些函數求導後能夠還原回f(t)

經過對速度求不定積分就能夠獲得位置函數,對加速度求則獲得的是速度函數:

\[\mathbf{p}(t)=\int\mathbf{v}(t)dt\\ \mathbf{v}(t)=\int\mathbf{a}(t)dt\\ \]

如今設加速度a(t)是一個恆定大小,方向,不隨時間變化的函數,而且咱們知道t=0時刻的初始位置p0和初始速度v0。所以速度函數能夠寫做:

\[\mathbf{v}(t)=\int\mathbf{a}(t)dt=t\cdot\mathbf{a} + \mathbf{c} \]

而t=0時刻知足

\[\mathbf{v}(0)=\mathbf{c}=\mathbf{v_0} \]

故速度函數爲:

\[\mathbf{v}(t) =t\cdot\mathbf{a} + \mathbf{v_0} \]

繼續積分並代入p(0),咱們能夠求出位置函數:

\[\mathbf{p}(t) = \frac{1}{2}t^2\mathbf{a}+t\mathbf{v_0}+\mathbf{p_0} \]

換句話說,粒子的運動軌跡p(t) (t>=0)徹底取決於初始位置、初始速度和恆定加速度。只要知道了這些參數,咱們就能夠畫出它的運動軌跡了。由於它是關於t的二次函數,故它的運動軌跡通常爲拋物線。

若令a=(0, -9.8, 0),則物體的運動能夠看作僅僅受到了重力的影響。

注意:你也能夠選擇不使用上面導出的函數。若是你已經知道了粒子的運動軌跡函數p(t),你也能夠直接用到你的程序當中。好比橢圓的參數方程等。

隨機性

在一個粒子系統中,咱們想要讓粒子的表現類似,但不是讓他們都同樣。這就意味着咱們須要給粒子系統加上隨機性。例如,若是咱們在模擬餘地,咱們想讓它從不一樣的地方降下來,以及稍微不一樣的下落角度、稍微不一樣的降落速度。

固然,若是隻是在C++中生成隨機數仍是一件比較簡單的事情的。但咱們還須要在着色器代碼中使用隨機數,而咱們沒有着色器可以直接使用的隨機數生成器。因此咱們的作法是建立一個1D紋理,裏面每一個元素是float4(使用DXGI_FORMAT_R32G32B32A32_FLOAT)。而後咱們使用區間[-1, 1]的隨機4D向量來填滿紋理,採樣的時候則使用wrap尋址模式便可。着色器經過對該紋理採樣來獲取隨機數。這裏有許多對隨機紋理進行採樣的方法。若是每一個粒子擁有不一樣的x座標,咱們可使用x座標來做爲紋理座標來獲取隨機數。然而,若是每一個粒子的x座標都相同,它們就會得到相同的隨機數。另外一種方式是,咱們可使用當前的遊戲時間值做爲紋理座標。這樣,不一樣時間生成的粒子將會得到不一樣的隨機值。這也意味着同一時間生成的粒子將會得到相同的隨機值。這樣若是粒子系統就不該該在同一時間生成多個粒子了。所以,咱們能夠考慮把二者結合起來,當系統在同一時刻生成許多粒子的時候,咱們能夠添加一個不一樣的紋理座標偏移值給遊戲時間。這樣就能夠儘量確保同一時間內產生的不一樣粒子在進行採樣的時候能得到不一樣的隨機數。例如,當咱們循環20次來建立出20個粒子的時候,咱們可使用循環的索引再乘上某個特定值做爲紋理座標的偏移,而後再進行採樣。如今咱們就可以拿到20個不一樣的隨機數了。

下面的代碼用來生成隨機數1D紋理:

須要注意的是,對於隨機數紋理,咱們只有一個mipmap等級。因此咱們得使用SampleLevel的採樣方法來限制採樣mipmap等級爲0。

下面的函數用於得到一個隨機的單位向量:

float3 RandUnitVec3(float offset)
{
    // 使用遊戲時間加上偏移值來從隨機紋理採樣
    float u = (g_GameTime + offset);
    // 份量均在[-1,1]
    float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz;
    // 標準化向量
    return normalize(v);
}

混合與粒子系統

粒子系統一般以某些混合形式來繪製。對於火焰和法術釋放的效果,咱們想要讓處於顆粒位置的顏色強度變量,那咱們可使用加法混合的形式。雖然咱們能夠只是將源顏色與目標顏色相加起來,可是粒子一般狀況下是透明的,咱們須要給源粒子顏色乘上它的alpha值。所以混合參數爲:

SrcBlend = SRC_ALPHA;
DestBlend = ONE;
BlendOp = ADD;

即混合等式爲:

\[\mathbf{C}=a_s\cdot\mathbf{C_{src}} + \mathbf{C_{dst}} \]

換句話說,源粒子給最終的混合顏色產生的貢獻程度是由它的不透明度所決定的:粒子越不透明,貢獻的顏色值越多。另外一種辦法是咱們能夠在紋理中預先乘上它的不透明度(由alpha通道描述),以便於稀釋它的紋理顏色。這種狀況下的混合參數爲:

SrcBlend = ONE;
DestBlend = ONE;
BlendOp = ADD;

加法混合還有一個很好的效果,那就是可使區域的亮度與那裏的粒子濃度成正比。濃度高的區域會顯得格外明亮,這一般也是咱們想要的

而對於煙霧來講,加法混合是行不通的,由於加入一堆重疊的煙霧粒子的顏色,最終會使得煙霧的顏色變亮,甚至變白。使用減法混合的話效果會更好一些(D3D11_BLEND_OP_REV_SUBTRACT),煙霧粒子會從目標色中減去一部分顏色。經過這種方式,可使高濃度的煙霧粒子區域會變得更加灰黑。雖然這樣作對黑煙的效果很好,可是對淺灰煙、蒸汽的效果表現不佳。煙霧的另外一種可能的混合方式是使用透明度混合,咱們只須要將煙霧粒子視做半透明物體,使用透明度混合來渲染它們。但透明度混合的主要問題是將系統中的粒子按照相對於眼睛的先後順序進行排序,這種作法很是昂貴且不實際。考慮到粒子系統的隨機性,這條規則有時候能夠打破,這並不會產生比較顯著的渲染問題。注意到若是場景中有許多粒子系統,這些系統應該按照從後到前的順序進行排序;但咱們也不想對系統內的粒子進行排序。

基於GPU的粒子系統

粒子系統通常隨着時間的推移會產生和消滅粒子。一種看起來比較合理的方式就是使用動態頂點緩衝區並在CPU跟蹤粒子的生成和消滅,頂點緩衝區裝有當前存活或者剛生成的粒子。可是,咱們從前面的章節已經知道一個獨立的、僅以流輸出階段爲輸出的一趟渲染就能夠在GPU徹底控制粒子的生成和摧毀。這種作法是很是高效的,由於它不須要從CPU上傳數據給GPU,而且它將粒子生成/摧毀的工做從CPU搬移到了GPU,而後能夠減小CPU的工做量了。

粒子系統特效

如今,咱們能夠將粒子的生成、變化、摧毀和繪製過程徹底寫在HLSL文件上,而不一樣的粒子系統的這些過程各有各的不一樣之處。好比說:

  1. 摧毀條件不一樣:咱們可能要在雨水打中地面的時候將它摧毀,然而對於火焰粒子來講它是在幾秒鐘後被摧毀
  2. 變化過程不一樣:煙霧粒子可能隨着時間推移變得暗淡,對於雨水粒子來講並非這樣的。一樣地,煙霧粒子隨時間推移逐漸擴散,而雨水大可能是往下掉落的。
  3. 繪製過程不一樣:線圖元一般在模擬雨水的時候效果良好,但火焰/煙霧粒子更多使用的是公告板的四邊形。
  4. 生成條件不一樣:雨水和煙霧的初始位置和速度的設置方式明顯也是不一樣的。

但這樣作好處是可讓C++代碼的工做量儘量地減到最小。

ParticleEffect類

按照慣例,粒子系統分爲了ParticleEffectParticleRender兩個部分。其中ParticleEffect對粒子系統的HLSL實現有所約束,它能夠讀取一套HLSL文件並負責數據的傳入。

class ParticleEffect : public IEffect
{
public:
	ParticleEffect();
	virtual ~ParticleEffect() override;

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

	// 初始化所需資源
	// 若effectPath爲HLSL/Fire
	// 則會尋找文件: 
	// - HLSL/Fire_SO_VS.hlsl
	// - HLSL/Fire_SO_GS.hlsl
	// - HLSL/Fire_VS.hlsl
	// - HLSL/Fire_GS.hlsl
	// - HLSL/Fire_PS.hlsl
	bool Init(ID3D11Device* device, const std::wstring& effectPath);

	// 產生新粒子到頂點緩衝區
	void SetRenderToVertexBuffer(ID3D11DeviceContext* deviceContext);
	// 繪製粒子系統
	void SetRenderDefault(ID3D11DeviceContext* deviceContext);

	void XM_CALLCONV SetViewProjMatrix(DirectX::FXMMATRIX VP);

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

	void SetGameTime(float t);
	void SetTimeStep(float step);

	void SetEmitDir(const DirectX::XMFLOAT3& dir);
	void SetEmitPos(const DirectX::XMFLOAT3& pos);

	void SetEmitInterval(float t);
	void SetAliveTime(float t);

	void SetTextureArray(ID3D11ShaderResourceView* textureArray);
	void SetTextureRandom(ID3D11ShaderResourceView* textureRandom);

	void SetBlendState(ID3D11BlendState* blendState, const FLOAT blendFactor[4], UINT sampleMask);
	void SetDepthStencilState(ID3D11DepthStencilState* depthStencilState, UINT stencilRef);

	void SetDebugObjectName(const std::string& name);

	// 
	// IEffect
	//

	// 應用常量緩衝區和紋理資源的變動
	void Apply(ID3D11DeviceContext* deviceContext) override;

private:
	class Impl;
	std::unique_ptr<Impl> pImpl;
};

其中用戶須要手動設置的有渲染時的混合狀態、深度/模板狀態,以及ViewProjEyePos。其他能夠交給接下來要講的ParticleRender類來完成。

ParticleRender類

該類表明一個粒子系統的實例,用戶須要設置與該系統相關的參數、使用的紋理等屬性:

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

	ParticleRender() = default;
	~ParticleRender() = default;
	// 不容許拷貝,容許移動
	ParticleRender(const ParticleRender&) = delete;
	ParticleRender& operator=(const ParticleRender&) = delete;
	ParticleRender(ParticleRender&&) = default;
	ParticleRender& operator=(ParticleRender&&) = default;

	// 自從該系統被重置以來所通過的時間
	float GetAge() const;

	void SetEmitPos(const DirectX::XMFLOAT3& emitPos);
	void SetEmitDir(const DirectX::XMFLOAT3& emitDir);

	void SetEmitInterval(float t);
	void SetAliveTime(float t);

	HRESULT Init(ID3D11Device* device, UINT maxParticles);
	void SetTextureArraySRV(ID3D11ShaderResourceView* textureArraySRV);
	void SetRandomTexSRV(ID3D11ShaderResourceView* randomTexSRV);

	void Reset();
	void Update(float dt, float gameTime);
	void Draw(ID3D11DeviceContext* deviceContext, ParticleEffect& effect, const Camera& camera);

	void SetDebugObjectName(const std::string& name);

private:
	
	UINT m_MaxParticles = 0;
	bool m_FirstRun = true;

	float m_GameTime = 0.0f;
	float m_TimeStep = 0.0f;
	float m_Age = 0.0f;

	DirectX::XMFLOAT3 m_EmitPos = {};
	DirectX::XMFLOAT3 m_EmitDir = {};

	float m_EmitInterval = 0.0f;
	float m_AliveTime = 0.0f;

	ComPtr<ID3D11Buffer> m_pInitVB;
	ComPtr<ID3D11Buffer> m_pDrawVB;
	ComPtr<ID3D11Buffer> m_pStreamOutVB;

	ComPtr<ID3D11ShaderResourceView> m_pTextureArraySRV;
	ComPtr<ID3D11ShaderResourceView> m_pRandomTexSRV;
	
};

注意:粒子系統使用一個紋理數組來對粒子進行貼圖,由於咱們可能不想讓全部的粒子看起來都是同樣的。例如,爲了實現一個煙霧的粒子系統,咱們可能想要使用幾種煙霧紋理來添加變化,圖元ID在像素着色器中能夠用來對紋理數組進行索引。

發射器粒子

由於幾何着色器負責建立/摧毀粒子,咱們須要一個特別的發射器粒子。發射器粒子自己能夠繪製出來,也能夠不被繪製。假如你想讓你的發射器粒子不能被看見,那麼在繪製時的幾何着色器的階段你就能夠不要將它輸出。發射器粒子與當前粒子系統中的其它粒子的行爲有所不一樣,由於它能夠產生其它粒子。例如,一個發射器粒子可能會記錄累計通過的時間,而且到達一個特定時間點的時候,它就會發射一個新的粒子。此外,經過限制哪些粒子能夠發射其它粒子,它讓咱們對粒子的發射方式有了必定的控制。好比說如今咱們只有一個發射器粒子,咱們能夠很方便地控制每一幀所生產的粒子數目。流輸出幾何着色器應當老是輸出至少一個發射器粒子,由於若是粒子系統丟掉了全部的發射器,粒子系統終究會消亡;但對於某些粒子系統來講,讓它最終消亡也許是一種理想的結果。

在本章中,咱們將只使用一個發射器粒子。但若是須要的話,當前粒子系統的框架也能夠進行擴展。

起始頂點緩衝區

在咱們的粒子系統中,有一個比較特別的起始頂點緩衝區,它僅僅包含了一個發射器粒子,而咱們用這個頂點緩衝區來啓動粒子系統。發射器粒子將會開始不停地產生其它粒子。須要注意的是起始頂點緩衝區僅僅繪製一次(除了系統被重置之外)。當粒子系統經發射器粒子啓動後,咱們就可使用兩個流輸出頂點緩衝區來進行後續繪製。

起始頂點緩衝區在系統被重置的時候也是有用的,咱們可使用下面的代碼來重啓粒子系統:

void ParticleRender::Reset()
{
    m_FirstRun = true;
    m_Age = 0.0f;
}

更新/繪製過程

繪製過程以下:

  1. 經過流輸出幾何着色器階段來更新當前幀的粒子
  2. 使用更新好的粒子進行渲染
void ParticleRender::Draw(ID3D11DeviceContext* deviceContext, ParticleEffect& effect, const Camera& camera)
{
    effect.SetGameTime(m_GameTime);
    effect.SetTimeStep(m_TimeStep);
    effect.SetEmitPos(m_EmitPos);
    effect.SetEmitDir(m_EmitDir);
    effect.SetEmitInterval(m_EmitInterval);
    effect.SetAliveTime(m_AliveTime);
    effect.SetTextureArray(m_pTextureArraySRV.Get());
    effect.SetTextureRandom(m_pRandomTexSRV.Get());

    // ******************
    // 流輸出
    //
    effect.SetRenderToVertexBuffer(deviceContext);
    UINT strides[1] = { sizeof(VertexParticle) };
    UINT offsets[1] = { 0 };

    // 若是是第一次運行,使用初始頂點緩衝區
    // 不然,使用存有當前全部粒子的頂點緩衝區
    if (m_FirstRun)
        deviceContext->IASetVertexBuffers(0, 1, m_pInitVB.GetAddressOf(), strides, offsets);
    else
        deviceContext->IASetVertexBuffers(0, 1, m_pDrawVB.GetAddressOf(), strides, offsets);

    // 通過流輸出寫入到頂點緩衝區
    deviceContext->SOSetTargets(1, m_pStreamOutVB.GetAddressOf(), offsets);
    effect.Apply(deviceContext);
    if (m_FirstRun)
    {
        deviceContext->Draw(1, 0);
        m_FirstRun = false;
    }
    else
    {
        deviceContext->DrawAuto();
    }

    // 解除緩衝區綁定
    ID3D11Buffer* nullBuffers[1] = { nullptr };
    deviceContext->SOSetTargets(1, nullBuffers, offsets);

    // 進行頂點緩衝區的Ping-Pong交換
    m_pDrawVB.Swap(m_pStreamOutVB);

    // ******************
    // 使用流輸出頂點繪製粒子
    //
    effect.SetRenderDefault(deviceContext);

    deviceContext->IASetVertexBuffers(0, 1, m_pDrawVB.GetAddressOf(), strides, offsets);
    effect.Apply(deviceContext);
    deviceContext->DrawAuto();
}

火焰

火焰粒子雖然是沿着指定方向發射,但給定了隨機的初速度來火焰四散,併產生火球。

// Fire.hlsli

cbuffer CBChangesEveryFrame : register(b0)
{
    matrix g_ViewProj;
    
    float3 g_EyePosW;
    float g_GameTime;
    
    float g_TimeStep;
    float3 g_EmitDirW;
    
    float3 g_EmitPosW;
    float g_EmitInterval;
    
    float g_AliveTime;
}

cbuffer CBFixed : register(b1)
{
    // 用於加速粒子運動的加速度
    float3 g_AccelW = float3(0.0f, 7.8f, 0.0f);
    
    // 紋理座標
    float2 g_QuadTex[4] =
    {
        float2(0.0f, 1.0f),
        float2(1.0f, 1.0f),
        float2(0.0f, 0.0f),
        float2(1.0f, 0.0f)
    };
}

// 用於貼圖到粒子上的紋理數組
Texture2DArray g_TexArray : register(t0);

// 用於在着色器中生成隨機數的紋理
Texture1D g_RandomTex : register(t1);

// 採樣器
SamplerState g_SamLinear : register(s0);


float3 RandUnitVec3(float offset)
{
	// 使用遊戲時間加上偏移值來採樣隨機紋理
    float u = (g_GameTime + offset);
	
	// 採樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
	
	// 投影到單位球
    return normalize(v);
}

#define PT_EMITTER 0
#define PT_FLARE 1

struct VertexParticle
{
    float3 InitialPosW : POSITION;
    float3 InitialVelW : VELOCITY;
    float2 SizeW : SIZE;
    float Age : AGE;
    uint Type : TYPE;
};

// 繪製輸出
struct VertexOut
{
    float3 PosW : POSITION;
    float2 SizeW : SIZE;
    float4 Color : COLOR;
    uint Type : TYPE;
};

struct GeoOut
{
    float4 PosH : SV_Position;
    float4 Color : COLOR;
    float2 Tex : TEXCOORD;
};
// Fire_SO_VS.hlsl
#include "Fire.hlsli"

VertexParticle VS(VertexParticle vIn)
{
    return vIn;
}
// Fire_SO_GS.hlsl
#include "Fire.hlsli"

[maxvertexcount(2)]
void GS(point VertexParticle gIn[1], inout PointStream<VertexParticle> output)
{
    gIn[0].Age += g_TimeStep;
    
    if (gIn[0].Type == PT_EMITTER)
    {
        // 是否到時間發射新的粒子
        if (gIn[0].Age > g_EmitInterval)
        {
            float3 vRandom = RandUnitVec3(0.0f);
            vRandom.x *= 0.5f;
            vRandom.z *= 0.5f;
            
            VertexParticle p;
            p.InitialPosW = g_EmitPosW.xyz;
            p.InitialVelW = 4.0f * vRandom;
            p.SizeW       = float2(3.0f, 3.0f);
            p.Age         = 0.0f;
            p.Type = PT_FLARE;
            
            output.Append(p);
            
            // 重置時間準備下一次發射
            gIn[0].Age = 0.0f;
        }
        
        // 老是保留髮射器
        output.Append(gIn[0]);
    }
    else
    {
        // 用於限制粒子數目產生的特定條件,對於不一樣的粒子系統限制也有所變化
        if (gIn[0].Age <= g_AliveTime)
            output.Append(gIn[0]);
    }
}
// Fire_VS.hlsl
#include "Fire.hlsli"

VertexOut VS(VertexParticle vIn)
{
    VertexOut vOut;
    
    float t = vIn.Age;
    
    // 恆定加速度等式
    vOut.PosW = 0.5f * t * t * g_AccelW + t * vIn.InitialVelW + vIn.InitialPosW;
    
    // 顏色隨着時間褪去
    float opacity = 1.0f - smoothstep(0.0f, 1.0f, t / 1.0f);
    vOut.Color = float4(1.0f, 1.0f, 1.0f, opacity);
    
    vOut.SizeW = vIn.SizeW;
    vOut.Type = vIn.Type;
    
    return vOut;
}
// Fire_GS.hlsl
#include "Fire.hlsli"

[maxvertexcount(4)]
void GS(point VertexOut gIn[1], inout TriangleStream<GeoOut> output)
{
    // 不要繪製用於產生粒子的頂點
    if (gIn[0].Type != PT_EMITTER)
    {
        //
        // 計算該粒子的世界矩陣讓公告板朝向攝像機
        //
        float3 look  = normalize(g_EyePosW.xyz - gIn[0].PosW);
        float3 right = normalize(cross(float3(0.0f, 1.0f, 0.0f), look));
        float3 up = cross(look, right);
        
        //
        // 計算出處於世界空間的四邊形
        //
        float halfWidth  = 0.5f * gIn[0].SizeW.x;
        float halfHeight = 0.5f * gIn[0].SizeW.y;
        
        float4 v[4];
        v[0] = float4(gIn[0].PosW + halfWidth * right - halfHeight * up, 1.0f);
        v[1] = float4(gIn[0].PosW + halfWidth * right + halfHeight * up, 1.0f);
        v[2] = float4(gIn[0].PosW - halfWidth * right - halfHeight * up, 1.0f);
        v[3] = float4(gIn[0].PosW - halfWidth * right + halfHeight * up, 1.0f);
    
        //
        // 將四邊形頂點從世界空間變換到齊次裁減空間
        //
        GeoOut gOut;
        [unroll]
        for (int i = 0; i < 4; ++i)
        {
            gOut.PosH  = mul(v[i], g_ViewProj);
            gOut.Tex   = g_QuadTex[i];
            gOut.Color = gIn[0].Color;
            output.Append(gOut);
        }
    }
}
// Fire_PS.hlsl
#include "Fire.hlsli"

float4 PS(GeoOut pIn) : SV_Target
{
    return g_TexArray.Sample(g_SamLinear, float3(pIn.Tex, 0.0f)) * pIn.Color;
}

在C++中,咱們還須要設置下面兩個渲染狀態用於粒子的渲染:

m_pFireEffect->SetBlendState(RenderStates::BSAlphaWeightedAdditive.Get(), nullptr, 0xFFFFFFFF);
m_pFireEffect->SetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);

雨水

雨水粒子系統也是由一系列的HLSL文件所組成。它的形式和火焰粒子系統有所類似,但在生成/摧毀/渲染的規則上有所不一樣。例如,咱們的雨水加速度是向下的,並帶有小幅度的傾斜角,然而火焰的加速度是向上的。此外,雨水粒子系統最終產生的繪製圖元是線,而不是四邊形;而且雨水的產生位置與攝像機位置有聯繫,它老是在攝像機的上方周圍(移動的時候在上方偏前)產生雨水粒子,這樣就不須要在整個世界產生雨水了。這樣就能夠形成一種當前正在下雨的假象(固然移動起來的話就會感受有些假,雨水量減小了)。須要注意該系統並無使用任何的混合狀態。

// Rain.hlsli

cbuffer CBChangesEveryFrame : register(b0)
{
    matrix g_ViewProj;
    
    float3 g_EyePosW;
    float g_GameTime;
    
    float g_TimeStep;
    float3 g_EmitDirW;
    
    float3 g_EmitPosW;
    float g_EmitInterval;
    
    float g_AliveTime;
}

cbuffer CBFixed : register(b1)
{
    // 用於加速粒子運動的加速度
    float3 g_AccelW = float3(-1.0f, -9.8f, 0.0f);
}

// 用於貼圖到粒子上的紋理數組
Texture2DArray g_TexArray : register(t0);

// 用於在着色器中生成隨機數的紋理
Texture1D g_RandomTex : register(t1);

// 採樣器
SamplerState g_SamLinear : register(s0);


float3 RandUnitVec3(float offset)
{
	// 使用遊戲時間加上偏移值來採樣隨機紋理
    float u = (g_GameTime + offset);
	
	// 採樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
	
	// 投影到單位球
    return normalize(v);
}

float3 RandVec3(float offset)
{
    // 使用遊戲時間加上偏移值來採樣隨機紋理
    float u = (g_GameTime + offset);
    
    // 採樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
    
    return v;
}

#define PT_EMITTER 0
#define PT_FLARE 1

struct VertexParticle
{
    float3 InitialPosW : POSITION;
    float3 InitialVelW : VELOCITY;
    float2 SizeW       : SIZE;
    float Age          : AGE;
    uint Type         : TYPE;
};

// 繪製輸出
struct VertexOut
{
    float3 PosW : POSITION;
    uint Type : TYPE;
};

struct GeoOut
{
    float4 PosH : SV_Position;
    float2 Tex : TEXCOORD;
};
// Rain_SO_VS.hlsl
#include "Rain.hlsli"

VertexParticle VS(VertexParticle vIn)
{
    return vIn;
}
// Rain_SO_GS.hlsl
#include "Rain.hlsli"

[maxvertexcount(6)]
void GS(point VertexParticle gIn[1], inout PointStream<VertexParticle> output)
{
    gIn[0].Age += g_TimeStep;
    
    if (gIn[0].Type == PT_EMITTER)
    {
        // 是否到時間發射新的粒子
        if (gIn[0].Age > g_EmitInterval)
        {
            [unroll]
            for (int i = 0; i < 5; ++i)
            {
                // 在攝像機上方的區域讓雨滴降落
                float3 vRandom = 30.0f * RandVec3((float)i / 5.0f);
                vRandom.y = 20.0f;
                
                VertexParticle p;
                p.InitialPosW = g_EmitPosW.xyz + vRandom;
                p.InitialVelW = float3(0.0f, 0.0f, 0.0f);
                p.SizeW       = float2(1.0f, 1.0f);
                p.Age         = 0.0f;
                p.Type        = PT_FLARE;
                
                output.Append(p);
            }
            
            // 重置時間準備下一次發射
            gIn[0].Age = 0.0f;
        }
        
        // 老是保留髮射器
        output.Append(gIn[0]);
    }
    else
    {
        // 用於限制粒子數目產生的特定條件,對於不一樣的粒子系統限制也有所變化
        if (gIn[0].Age <= g_AliveTime)
            output.Append(gIn[0]);
    }
}
// Rain_VS.hlsl
#include "Rain.hlsli"

VertexOut VS(VertexParticle vIn)
{
    VertexOut vOut;
    
    float t = vIn.Age;
    
    // 恆定加速度等式
    vOut.PosW = 0.5f * t * t * g_AccelW + t * vIn.InitialVelW + vIn.InitialPosW;
    
    vOut.Type = vIn.Type;
    
    return vOut;
}
// Rain_GS.hlsl
#include "Rain.hlsli"

[maxvertexcount(6)]
void GS(point VertexOut gIn[1], inout LineStream<GeoOut> output)
{
    // 不要繪製用於產生粒子的頂點
    if (gIn[0].Type != PT_EMITTER)
    {
        // 使線段沿着一個加速度方向傾斜
        float3 p0 = gIn[0].PosW;
        float3 p1 = gIn[0].PosW + 0.07f * g_AccelW;
        
        GeoOut v0;
        v0.PosH = mul(float4(p0, 1.0f), g_ViewProj);
        v0.Tex = float2(0.0f, 0.0f);
        output.Append(v0);
        
        GeoOut v1;
        v1.PosH = mul(float4(p1, 1.0f), g_ViewProj);
        v1.Tex = float2(0.0f, 0.0f);
        output.Append(v1);
    }
}
// Rain_PS.hlsl
#include "Rain.hlsli"

float4 PS(GeoOut pIn) : SV_Target
{
    return g_TexArray.Sample(g_SamLinear, float3(pIn.Tex, 0.0f));
}

演示

下面的動圖演示了火焰和雨水的粒子系統效果:

練習題

  1. 實現一個爆炸的粒子系統。發射器粒子產生N個隨機方向的外殼粒子。在通過一個短暫時間後,每一個外殼粒子應當爆炸產生M個粒子。每一個外殼不須要在同一個時間發生爆炸——經過隨機性賦上不一樣的爆炸倒計時。對M和N進行測試直到你獲得不錯的結果。注意發射器在產生全部的外殼粒子後,將其摧毀使得不要產生更多的外殼。
  2. 實現一個噴泉的粒子系統。這些粒子應當從某個點產生,並沿着圓錐體範圍內的隨機方向向上發射。最終重力會使得它們掉落到地面。注意:給粒子一個足夠高的初速度來讓它們克服重力。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索