前言:最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于DirectSDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用WINDOWSDK来实现.
顺手安利一波:我最近在学DX11 with WindowSDK 教程
博客地址:
https://www.cnblogs.com/X-Jun/p/9028764.html
所以下面的都是基于这个教程和上面提到的那本书来写的,推荐看到教程17章和书里第15章之后再来看这篇博客,有助于更好的理解
PS:这篇博客记录下我怎么去实现一个粒子系统并且用一个粒子系统去实现一个简单的雨特效和记录下自己的理解,一些具体知识点并不会在此提到.
粒子系统
粒子属性:
1.粒子生成速度(即单位时间粒子生成的数量)
2.粒子初始速度向量
3.粒子寿命
4.粒子颜色
5.其他一些特定的参数
粒子系统更新循环:模拟阶段和绘制阶段
粒子系统使用Billboard技术来进行纹理映射和图元渲染
定义粒子的顶点结构
struct ParticleVertex
{
XMFLOAT3 initialPos; //粒子的初始中心位置
XMFLOAT3 initialVel; //粒子的初始速度
XMFLOAT3 size; //粒子的大小
float age; //粒子的存活时间
uint type; //粒子的类型
}
这里粒子的类型包括触发器粒子和渲染粒子
1.触发器粒子在粒子系统中只有一个,用于粒子的生成,不会被绘制
粒子的位置函数(推导过程忽略)
p(t)=(1/2)*a*t^2+v0*t+p0
每个粒子有自己的随机行为,所以我们引用了一个工具类--Random类来为粒子创建一些随机函数,下面的雨的实现就是利用了这个Random类来产生随机位置
产生随机纹理的函数和着色器里面的实现在这就不详细讲了,因为在书上有具体代码
若没有实现随机位置,雨就只有一条直线
雨的粒子特效实现
用雨的特效实现来理解粒子系统
ParticleEffect(粒子框架)
这个框架根据博客里面的BasicEffect进行编写的
我们先来看着色器
着色器头文件(Particle.hlsli)
Texture2D g_Tex : register(t0);
Texture1D g_Random : register(t1);
SamplerState g_Sam : register(s0);
cbuffer CBChangesEveryFrame : register(b0)
{
float3 gEmitPosW;
float gGameTime;
float3 gEmitDirW;
float gTimeStep;
matrix g_View;
}
cbuffer CBChangesOnResize : register(b1)
{
matrix g_Proj;
}
struct Particle
{
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;
};
一.因为绘制粒子的着色器和原来的不同,所以这里用到了两个顶点着色器,两个几何着色器,一个像素着色器
1.首先两个顶点着色器的输入布局都是相同的
const D3D11_INPUT_ELEMENT_DESC Particle::inputLayout[5] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "VELOCITY", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "SIZE", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "AGE", 0, DXGI_FORMAT_R32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TYPE", 0, DXGI_FORMAT_R32_UINT, 0, 36, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
着色器头文件中另一个结构的布局如下:
const D3D11_INPUT_ELEMENT_DESC Vertexout::inputLayout[2] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TYPE", 0, DXGI_FORMAT_R32_UINT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
第一个顶点着色器(ParticleSO_VS.hlsl)没有做任何处理,然后第二个(ParticleSO_VS.hlsl)即为实现上面那个粒子的位置函数
2.然后下一步到几何着色器做处理,第一个几何着色器(ParticleSO_GS.hlsl)是用于流输出更新粒子的,第二个(ParticleDW_GS.hlsl是实现用于渲染粒子
1.第一个几何着色器代码
[maxvertexcount(6)]
void GS(
point Particle gin[1],
inout PointStream<Particle> ptStream)
{
gin[0].Age += gTimeStep;
if(gin[0].Type==0)
{
if (gin[0].Age >= 0.002f)
{
for (int i = 0; i < 5;i++)
{
float3 vRandom = 35.0f * RandVec3((float) i / 5.0f);
vRandom.y = 20.0f;
Particle p;
p.InitialPosW = gEmitPosW.xyz + vRandom;
p.InitialVelW = float3(0.0f, 0.0f, 0.0f);
p.SizeW = float2(1.0f, 1.0f);
p.Age = 0.0f;
p.Type = 1;
ptStream.Append(p);
}
//重置发射时间
gin[0].Age = 0.0f;
}
//总是保持发射器
ptStream.Append(gin[0]);
}
else
{
**//指定保存粒子的条件;age>3.0f即销毁粒子**
if (gin[0].Age <= 3.0f)
ptStream.Append(gin[0]);
}
}
第二个几何着色器
//公告板技术
#include"Particle.hlsli"
[maxvertexcount(2)]
void GS(
point Vertexout gin[1],
inout LineStream<GeoOut> lineStream)(这里是线图元,因为是雨)
{
//不要绘制发射器粒子。
if(gin[0].Type!=0)
{
//向加速度方向倾斜的直线。
float3 po = gin[0].PosW;
float3 p1 = gin[0].PosW + 0.07f * (float3(-1.0f, -9.8f, 0.0f));
matrix viewProj = mul(g_View, g_Proj);
GeoOut v0;
v0.PosH = mul(float4(po, 1.0f), viewProj);
v0.Tex = float2(0.0f, 0.0f);
lineStream.Append(v0);
GeoOut v1;
v1.PosH = mul(float4(p1, 1.0f), viewProj);
v1.Tex = float2(1.0f, 1.0f);
lineStream.Append(v1);
}
}
3.最后的像素着色器就实现简单的纹理采样
PS:最后说明下这只是针对雨粒子的着色器具体实现,不同的粒子有不同的着色器具体实现
二丶设置绘制状态(相当于把这些东西绑定到渲染管线上)
1.流输出更新粒子的绘制状态
void ParticleEffect::SetRenderStreamOutParticle(ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
UINT stride = sizeof(Particle);
UINT offset = 0;
ID3D11Buffer* nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset);
deviceContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_POINTLIST);
deviceContext->IASetInputLayout(pImpl->m_pParticleLayout.Get());
deviceContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);
deviceContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
deviceContext->VSSetShader(pImpl->m_pParticleVS.Get(), nullptr, 0);
deviceContext->GSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->GSSetShader(pImpl->m_pParticleSOGS.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSWireframe.Get());
deviceContext->PSSetShader(nullptr, nullptr, 0);
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditiveP.Get(), nullptr, 0xFFFFFFFF);
}
上面的主要是把下面的着色器绑定到渲染管线上并且开启流输出
并且要为GS设置一个采样器,因为在GS着色器中会用到采样器,采样器进行1D随机纹理采样产生[-1,1]的向量
若用采用了2D的纹理SampleLevel方法(如雨的纹理),容易会产生下面的效果
因为这是在一个固定的范围之内进行了采样,导致了在雨的范围狭小(我的理解),详情可以查官方文档了解
用一开始提到的书里面的代码便可以解决
2.默认的绘制状态
void ParticleEffect::SetRenderDefault(ComPtr<ID3D11DeviceContext> deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_POINTLIST);
deviceContext->IASetInputLayout(pImpl->m_pParticleLayout.Get());
deviceContext->VSSetShader(pImpl->m_pParticleDWVS.Get(), nullptr, 0);
// 关闭流输出
deviceContext->GSSetShader(pImpl->m_pParticleDWGS.Get(), nullptr, 0);
ID3D11Buffer* bufferArray[1] = { nullptr };
UINT offset = 0;
deviceContext->SOSetTargets(1, bufferArray, &offset);
deviceContext->RSSetState(RenderStates::RSWireframe.Get());
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->PSSetShader(pImpl->m_pParticlePS.Get(), nullptr, 0);
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditiveP.Get(), nullptr, 0xFFFFFFFF);
}
上面的代码表示把下面的这些着色器绑定到渲染管线上
因为上面有用到流输出,所以我们要关闭了这个流输出
PS:光栅化用线框模式,不用深度测试,混合状态用下面的混合公式:
Color = SrcAlpha *SrcColor + DestColor
不同的粒子会使用不同的混合公式,是具体情况而视
三丶应用缓冲区、纹理资源和进行更新
直接看函数把
void ParticleEffect::Apply(ComPtr<ID3D11DeviceContext> deviceContext)
{
auto& pCBuffers = pImpl->m_pCBuffers;
// 将缓冲区绑定到渲染管线上
pCBuffers[0]->BindGS(deviceContext);
pCBuffers[1]->BindGS(deviceContext);
// 设置SRV
deviceContext->PSSetShaderResources(0, 1, pImpl->m_pTexture.GetAddressOf()); //把资源视图绑定到PS上,并且对于槽(t0)
deviceContext->GSSetShaderResources(1, 1, pImpl->m_pRamTexture.GetAddressOf()); //把资源视图绑定到GS上,并且对于槽(t1)
if (pImpl->m_IsDirty)
{
pImpl->m_IsDirty = false;
for (auto& pCBuffer : pCBuffers)
{
pCBuffer->UpdateBuffer(deviceContext);
}
}
}
四丶我们弄好了粒子的框架,那么我们怎么去实现粒子的更新和绘制呢?
首先我们要更新每帧改变的常量缓冲区
cbuffer CBChangesEveryFrame : register(b0)
{
float3 gEmitPosW;
float gGameTime;
float3 gEmitDirW;
float gTimeStep;
matrix g_View;
}
在UpdateScene函数中我们要更新这个缓冲区里面的数据,在ParticleEffect实现一些方法,然后再UpdateScene里面调用它们
然后我们每次调用绘制时使用Effect框架Apply方法来应用缓冲区、纹理资源并进行更新
下面便是实现粒子绘制
1.首先我们要创建3个顶点缓冲区,一个用于初始化,另两个用于更新和绘制
// 设置顶点缓冲区描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_DEFAULT; // 这里需要允许流输出阶段通过GPU写入
vbd.ByteWidth = sizeof(Particle);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER ;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
Particle p;
ZeroMemory(&p, sizeof(Particle));
p.Age = 0.0f;
p.Type = 0;
// 新建顶点缓冲区
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = &p;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); //用于初始化
vbd.ByteWidth = sizeof(Particle)*m_MaxParticles; //m_maxparticle是最大的粒子数
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[1].ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[2].ReleaseAndGetAddressOf()));
2.然后进行绘制函数的实现,下面是进行一个绘制过程理解
(1)第一帧初始化时首先先设置流输出绘制状态
m_ParticleEffect.SetRenderStreamOutParticle(m_pd3dImmediateContext, m_pVertexBuffers[0], m_pVertexBuffers[1]);
(2)然后使用
m_pd3dImmediateContext->Draw(1, 0);
把粒子实现更新(初始化),即把更新后的粒子数据流输出到第二个缓冲区
(3)最后设置设置默认状态并用
m_ParticleEffect.SetRenderDefault(m_pd3dImmediateContext);
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffers[inputIndex].GetAddressOf(), &stride, &offset);
当前的inpurIndex为1,即表示第二个缓冲区绑定到渲染管线上,然后使用
m_pd3dImmediateContext->DrawAuto();
用第二个缓冲区(即更新后的)来实现粒子的绘制
PS:从上面博客的流输出篇,可以理解到DrawAuto要求第一次流输出绘制时使用Draw、DrawIndexed系列的方法,后面使用DrawAuto即可以不使用参数来正确绘制
3.(1)第二帧开始可以便不再使用第一个顶点缓冲区,只要第二和第三个即可以了
然后就设置流输出状态绘制(即更新)(inputIndex当前为1)
m_ParticleEffect.SetRenderStreamOutParticle(m_pd3dImmediateContext, m_pVertexBuffers[inputIndex], m_pVertexBuffers[inputIndex % 2 + 1]);
(2)然后使用DrawAuto把第二个缓冲区更新数据流输出到第三个缓冲区
(3)最后绑定第三个顶点缓冲区调用DrawAuto来进行绘制
4.后面的帧数只是把第二和第三个顶点缓冲区不断进行和第二帧一样的步骤
用最新的缓冲区数据设置流输出绘制流输出更新到另一个缓冲区,这时另一个缓冲区变为最新的缓冲区,然后设置默认绘制并绑定最新的缓冲区来进行绘制