DirectX11 With Windows SDK--35 粒子系统
前言
在这一章中,我们主要关注的是如何模拟一系列粒子,并控制它们运动。这些粒子的行为都是类似的,但它们也带有一定的随机性。这一堆粒子的几何我们叫它为粒子系统,它可以被用于模拟一些比较现象,如:火焰、雨、烟雾、爆炸、法术效果等。
在这一章开始之前,你需要先学过如下章节:
章节 |
---|
11 混合状态 |
15 几何着色器初探 |
16 流输出阶段 |
17 利用几何着色器实现公告板效果 |
学习目标:
- 熟悉如何利用几何着色器和流输出阶段来高效存储、渲染粒子
- 了解我们如何利用基本的物理概念来让我们的粒子能够以物理上的真实方式来运动
- 设计一个灵活的粒子系统框架使得我们可以方便地创建新的自定义粒子系统
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
粒子的表示
粒子是一种非常小的对象,通常在数学上可以表示为一个点。因此我们在D3D中可以考虑使用图元类型D3D11_PRIMITIVE_TOPOLOGY_POINTLIST
来将一系列点传入。然而,点图元仅仅会被光栅化为单个像素。考虑到粒子可以有不同的大小,并且甚至需要将整个纹理都贴到这些粒子上,我们将采用前面公告板的绘制策略,即在顶点着色器输出顶点,然后在几何着色器将其变成一个四边形并朝向摄像机。此外需要注意的是,这些粒子的y轴是与摄像机的y轴是对齐的。
如果我们知道世界坐标系的上方向轴向量j,公告板中心位置C,以及摄像机位置E,这样我们可以描述公告板在世界坐标系下的局部坐标轴(即粒子的世界变换)是怎样的:
\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时刻的瞬时速度为:
\]
同样,粒子在t时刻的瞬时加速度为:
\]
学过高数的话下面的公式不难理解:
[\mathbf{F}(t)+C]'=\mathbf{f}(t)
\]
连续函数f(t)的不定积分得到的函数有无限个,即C可以为任意常数,这些函数求导后可以还原回f(t)
通过对速度求不定积分就可以得到位置函数,对加速度求则得到的是速度函数:
\mathbf{v}(t)=\int\mathbf{a}(t)dt\\
\]
现在设加速度a(t)是一个恒定大小,方向,不随时间变化的函数,并且我们知道t=0时刻的初始位置p0和初始速度v0。因此速度函数可以写作:
\]
而t=0时刻满足
\]
故速度函数为:
\]
继续积分并代入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;
即混合等式为:
\]
换句话说,源粒子给最终的混合颜色产生的贡献程度是由它的不透明度所决定的:粒子越不透明,贡献的颜色值越多。另一种办法是我们可以在纹理中预先乘上它的不透明度(由alpha通道描述),以便于稀释它的纹理颜色。这种情况下的混合参数为:
SrcBlend = ONE;
DestBlend = ONE;
BlendOp = ADD;
加法混合还有一个很好的效果,那就是可以使区域的亮度与那里的粒子浓度成正比。浓度高的区域会显得格外明亮,这通常也是我们想要的
而对于烟雾来说,加法混合是行不通的,因为加入一堆重叠的烟雾粒子的颜色,最终会使得烟雾的颜色变亮,甚至变白。使用减法混合的话效果会更好一些(D3D11_BLEND_OP_REV_SUBTRACT
),烟雾粒子会从目标色中减去一部分颜色。通过这种方式,可以使高浓度的烟雾粒子区域会变得更加灰黑。虽然这样做对黑烟的效果很好,但是对浅灰烟、蒸汽的效果表现不佳。烟雾的另一种可能的混合方式是使用透明度混合,我们只需要将烟雾粒子视作半透明物体,使用透明度混合来渲染它们。但透明度混合的主要问题是将系统中的粒子按照相对于眼睛的前后顺序进行排序,这种做法非常昂贵且不实际。考虑到粒子系统的随机性,这条规则有时候可以打破,这并不会产生比较显著的渲染问题。注意到如果场景中有许多粒子系统,这些系统应该按照从后到前的顺序进行排序;但我们也不想对系统内的粒子进行排序。
基于GPU的粒子系统
粒子系统一般随着时间的推移会产生和消灭粒子。一种看起来比较合理的方式就是使用动态顶点缓冲区并在CPU跟踪粒子的生成和消灭,顶点缓冲区装有当前存活或者刚生成的粒子。但是,我们从前面的章节已经知道一个独立的、仅以流输出阶段为输出的一趟渲染就可以在GPU完全控制粒子的生成和摧毁。这种做法是非常高效的,因为它不需要从CPU上传数据给GPU,并且它将粒子生成/摧毁的工作从CPU搬移到了GPU,然后可以减少CPU的工作量了。
粒子系统特效
现在,我们可以将粒子的生成、变化、摧毁和绘制过程完全写在HLSL文件上,而不同的粒子系统的这些过程各有各的不同之处。比如说:
- 摧毁条件不同:我们可能要在雨水打中地面的时候将它摧毁,然而对于火焰粒子来说它是在几秒钟后被摧毁
- 变化过程不同:烟雾粒子可能随着时间推移变得暗淡,对于雨水粒子来说并不是这样的。同样地,烟雾粒子随时间推移逐渐扩散,而雨水大多是往下掉落的。
- 绘制过程不同:线图元通常在模拟雨水的时候效果良好,但火焰/烟雾粒子更多使用的是公告板的四边形。
- 生成条件不同:雨水和烟雾的初始位置和速度的设置方式明显也是不同的。
但这样做好处是可以让C++代码的工作量尽可能地减到最小。
ParticleEffect类
按照惯例,粒子系统分为了ParticleEffect
和ParticleRender
两个部分。其中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;
};
其中用户需要手动设置的有渲染时的混合状态、深度/模板状态,以及ViewProj
和EyePos
。其余可以交给接下来要讲的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;
}
更新/绘制过程
绘制过程如下:
- 通过流输出几何着色器阶段来更新当前帧的粒子
- 使用更新好的粒子进行渲染
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));
}
演示
下面的动图演示了火焰和雨水的粒子系统效果:
练习题
- 实现一个爆炸的粒子系统。发射器粒子产生N个随机方向的外壳粒子。在经过一个短暂时间后,每个外壳粒子应当爆炸产生M个粒子。每个外壳不需要在同一个时间发生爆炸——通过随机性赋上不同的爆炸倒计时。对M和N进行测试直到你得到不错的结果。注意发射器在产生所有的外壳粒子后,将其摧毁使得不要产生更多的外壳。
- 实现一个喷泉的粒子系统。这些粒子应当从某个点产生,并沿着圆锥体范围内的随机方向向上发射。最终重力会使得它们掉落到地面。注意:给粒子一个足够高的初速度来让它们克服重力。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
DirectX11 With Windows SDK--35 粒子系统的更多相关文章
- 粒子系统与雨的效果 (DirectX11 with Windows SDK)
前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...
- DirectX11 With Windows SDK--26 计算着色器:入门
前言 现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述. DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群 ...
- DirectX11 With Windows SDK--33 曲面细分阶段(Tessellation)
前言 曲面细分是Direct3D 11带来的其中一项重要的新功能.它引入了两个可编程着色器阶段以及一个固定的镶嵌处理过程.简单来说,曲面细分技术可以将几何体细分为更小的三角形,并以某种方式把这些新生成 ...
- DirectX11 With Windows SDK--10 摄像机类
前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境 ...
- DirectX11 With Windows SDK--27 计算着色器:双调排序
前言 上一章我们用一个比较简单的例子来尝试使用计算着色器,但是在看这一章内容之前,你还需要了解下面的内容: 章节 26 计算着色器:入门 深入理解与使用缓冲区资源(结构化缓冲区/有类型缓冲区) Vis ...
- DirectX11 With Windows SDK--25 法线贴图
前言 在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色.但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应 ...
- DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用
前言 尽管在上一章的动态天空盒中用到了Render-To-Texture技术,但那是针对纹理立方体的特化实现.考虑到该技术的应用层面非常广,在这里抽出独立的一章专门来讲有关它的通用实现以及各种应用. ...
- DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现
前言 上一章的静态天空盒已经可以满足绝大部分日常使用了.但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了.因此我们需要在运 ...
- DirectX11 With Windows SDK--01 DirectX11初始化
前言 由于个人觉得龙书里面第4章提供的Direct3D 初始化项目封装得比较好,而且DirectX SDK Samples里面的初始化程序过于精简,不适合后续使用,故选择了以Init Direct3D ...
随机推荐
- Redis源码阅读一:简单动态字符串SDS
源码阅读基于Redis4.0.9 SDS介绍 redis 127.0.0.1:6379> SET dbname redis OK redis 127.0.0.1:6379> GET dbn ...
- 再看rabbitmq的交换器和队列的关系
最近又要用到rabbitmq,业务上要求服务器只发一次消息,需要多个客户端都去单独消费.但我们知道rabbitmq的机制里,每个队列里的消息只能消费一次,所以客户端要单独消费信息,就必须得每个客户端单 ...
- redis cluster集群中键的分布算法
Redis Cluster Redis Cluster是Redis的作者 Antirez 提供的 Redis 集群方案 —— 官方多机部署方案,每组Redis Cluster是由多个Redis实例组成 ...
- idea的maven项目无法引入junit类
本机:java版本:1.8 pom中是junit版本:4.12 出现问题:在使用@Test 无法引入 : org.junit.Test; 解决方法:junit在pom.xml改为 4.12-beta- ...
- JavaWeb网上图书商城完整项目--12.项目所需jquery函数介绍之ajax
jquery中使用ajax发送异步请求 下面的一个案例在input输入框失去焦点的时候发送一个异步的请求: 我们来看程序的案例: 这里要强调的是返回值最好选择是json,json对应的就是对象,Jav ...
- 深入理解React:事件机制原理
目录 序言 DOM事件流 事件捕获阶段.处于目标阶段.事件冒泡阶段 addEventListener 方法 React 事件概述 事件注册 document 上注册 回调函数存储 事件分发 小结 参考 ...
- YoyoGo基于ASP.NET Core设计的Golang实现
YoyoGo YoyoGo 是一个用 Go 编写的简单,轻便,快速的 微服务框架,目前已实现了Web框架的能力,但是底层设计已支持. Github https://github.com/yoyofx/ ...
- (私人收藏)2019科协WER解决方案
2019科协WER解决方案 含地图,解决程序,详细规则,搭建方案EV3;乐高;机器人比赛;能力风暴;WER https://pan.baidu.com/s/16sdFmM49bPijYw55i8ox1 ...
- 通过原生js对DOM事件的绑定的几种方式总汇
在网页开发中经常会有交互操作,比如点击一个dom元素,需要让js对该操作做出相应的响应,这就需要对Dom元素进行事件绑定来进行处理,js通常有三种常用的方法进行事件绑定:在DOM元素中直接绑定:在Ja ...
- 「区间DP」「洛谷PP3146 」[USACO16OPEN]248 G
[USACO16OPEN]248 G 题目: 题目描述 Bessie likes downloading games to play on her cell phone, even though sh ...