前言
最近在學粒子系統,看這之前的<<3D圖形編程基礎 基於DirectX 11 >>是基於Direct SDK的,而DXSDK微軟已經很久沒有更新過了並且我學的DX11是用Windows SDK來實現.
順手安利一波:我最近在學DirectX11 with Windows SDK 教程
博客地址: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 },
};
第一個頂點着色器(Particle_VS.hlsl)沒有做任何處理,然后第二個(ParticleDW_VS.hlsl)即為實現上面那個粒子的位置函數
2.然后下一步到幾何着色器做處理,第一個幾何着色器(ParticleSO_GS.hlsl)是用於流輸出更新粒子的,第二個(ParticleDW_GS.hlsl是實現用於渲染粒子
1.第一個幾何着色器(ParticleSO_GS.hlsl)代碼
[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]);
}
}
第二個幾何着色器(ParticleDW_GS.hlsl)
//公告板技術
#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]的3D向量來產生大范圍隨機位置
若對給定的2D紋理(如這個雨的紋理)用SampleLevel方法,容易會產生下面的效果

這是因為對雨的紋理采樣,所采樣得到的位置是固定的(雨的紋理信息是固定的),所以產生了上面這種狹小范圍的雨
因初始化生成了隨機1D紋理進行采樣產生大范圍的隨機位置
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);
當前的inputIndex為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.后面的幀數只是把第二和第三個頂點緩沖區不斷進行和第二幀一樣的步驟
用最新的緩沖區數據設置流輸出繪制流輸出更新到另一個緩沖區,這時另一個緩沖區變為最新的緩沖區,然后設置默認繪制並綁定最新的緩沖區來進行繪制
具體的C++代碼自己實現下,畢竟踩過的坑才是自己的
最后便能用粒子系統實現雨的效果,下面是效果顯示

作者:Ligo丶
出處:https://www.cnblogs.com/Ligo-Z/
本文版權歸作者和博客園共有,歡迎轉載,但必須給出原文鏈接,並保留此段聲明,否則保留追究法律責任的權利。
