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,以及有什么问题也能够在这里汇报。

相关文章
相关标签/搜索