20-粒子系統和輸出流

在本章中,我們關心的是對一組粒子(通常很小)進行建模的任務,這些粒子的行爲都類似但有點隨機; 我們稱這樣的粒子集合爲粒子系統。 粒子系統可用於模擬各種各樣的現象,如火災,雨水,煙霧,爆炸,噴水,魔法效果和射彈。
目標:
1.學習如何使用幾何着色器和流出功能有效地存儲和渲染粒子。
2.瞭解我們如何使用基本的物理概念使我們的粒子以物理逼真的方式運動。
3.設計靈活的粒子系統框架,可以輕鬆創建新的定製粒子系統。

20.1粒子表示

粒子是一個非常小的物體,通常以數學方式將其建模爲點。 因此,一個點基元(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST)將成爲顯示粒子的理想選擇。 但是,點基元被光柵化爲單個像素。 這不會給我們太大的靈活性,因爲我們希望有各種尺寸的粒子,甚至將整個紋理映射到這些粒子上。 因此,我們將採用一種類似於第11章樹形廣告牌的策略:我們將使用點存儲粒子,然後將它們展開爲幾何着色器中面向相機的四邊形。 與樹形廣告牌相比,它們與世界y軸一致,而廣告牌則完全面向相機(見圖20.1)。

如果我們知道世界上向量j,廣告牌的中心位置C和眼睛位置E的世界座標,那麼我們可以用世界座標來描述廣告牌的本地框架,這就給了我們廣告牌的世界矩陣:

w=EC||EC||u=j×w||j×w||v=w×uw=[uxuyuz0vxvyvz0wxwywz0CxCyCz1]


圖20.1 世界和廣告牌框架。廣告牌面向眼睛位置E.

除了位置和大小之外,我們的粒子還有其他屬性。 我們的粒子頂點結構如下所示:

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

NOTE:我們不需要將點擴大到四分之一。 例如,使用線條列表渲染雨水效果非常好。我們可以使用不同的幾何着色器將點展開爲線條。 基本上,在我們的系統中,每個粒子系統都會有自己的效果文件。 效果文件實現特定於與其相關聯的特定類型的粒子系統的細節。

20.2粒子運動
我們希望我們的粒子以真實逼真的方式移動。 爲了簡單起見,在本書中,我們將自己限制在一個恆定的淨加速度; 例如,由於重力引起的加速度。 (我們也可以通過使其他力的加速度也是恆定的,比如風來做出鬆散的近似值)。另外,我們不會使用我們的粒子進行任何碰撞檢測。

令p(t)描述沿着曲線移動的粒子(在時間t)的位置。時間t時粒子的瞬時速度爲:

v(t)=p(t)

時間t處粒子的瞬時加速度爲:
a(t)=v(t)=p(t)

從微積分回憶以下內容:
1.函數f(t)的一個反導函數是任意函數F(t),使得F(t)的導數是f(t); 即F’(t)= f(t)。

2.如果F(t)是f(t)的任何一個反導函數,c是任意常數,那麼F(t)+ c也是f(t)的一個反導函數。 此外,f(t)的每個反導因子都有F(t)+ c的形式。

3.爲了表示f(t)的任意一個導數,我們使用積分表示法
∫f(t)dt = F(t)+ c。
從速度和加速度的定義可以明顯看出,速度函數是加速函數的反導函數,而位置函數是速度函數的反導函數。 因此我們有:

p(t)=v(t)dtv(t)=a(t)dt

現在,假定加速度是恆定的(即它不隨時間變化)。假設我們知道時間t=0時的初始粒子速度v(0)=v0 和初始粒子位置p(0)=p0 。然後通過積分恆定加速度得到速度函數:
v(t)=a(t)dt=ta+c

爲了找到常數c,我們使用我們的初始速度:
v(0)=0·a+c=c=v0

所以速度函數爲
v(t)=ta+v0

爲了找到位置函數,我們整合剛發現的速度函數:
p(t)=v(t)dt=(ta+v0)dt=12t2a+tv0+k

爲了找到常數k,我們使用我們的初始位置:
p(0)=12·0·a+0·v0+k=k=P0

所以位置函數是
p(t)=12t2a+tv0+p0

換句話說,粒子的軌跡p(t)(即在任何時刻t≥0時的位置)完全由其初始位置,初始速度和加速度常數決定。 這是合理的,因爲如果我們知道我們從哪裏開始,我們開始朝哪個方向走多快和多快,並且我們知道我們如何在所有時間加速,那麼我們應該能夠找出我們遵循的道路。

讓我們看一個例子。 假設你有一個小型大炮坐在座標系的原點,瞄準從x軸測量的30°角(見圖20.2)。 所以在這個座標系中,p0 =(0,0,0)(即炮彈的初始位置在原點),由重力引起的恆定加速度爲a =(0,-9.8,0)m / s2(即,由於重力引起的加速度爲每秒9.8平方米)。 另外,假設從以前的測試中,我們已經確定在大炮發生火災時,炮彈球的初始速度爲每秒50米。 因此,初始速度是v0 = 50(cos30°,sin30°,0)m / s≈(43.3,25.0,0)m / s [記住速度是速度和方向,所以我們將速度乘以單位方向矢量 (cos 30°,sin30°,0)]。 因此炮彈的軌跡由下式給出:


圖20.2。 粒子在xy平面中隨時間推移的路徑(時間維度未顯示),給定初始位置和速度,並且由於重力而經歷恆定的加速度。

p(t)=12t2a+tv0+p0=12t2(0,9.8,0)m/s2+t(43.3,25.0,0)m/s

如果我們在xy平面中繪製這個圖(z座標總是爲零),我們得到圖20.2,這是我們期望的重力軌跡。

NOTE:您也可以選擇不使用之前派生的函數。 如果你已經知道你想要粒子採用的軌跡函數p(t),那麼你可以直接編程它。 例如,如果你想讓你的粒子遵循一個橢圓軌道,那麼你可以使用p(t)的橢圓參數方程。

20.3 隨機性

在粒子系統中,我們希望粒子的行爲類似,但不完全相同; 換句話說,我們想爲系統添加一些隨機性。 例如,如果我們在模擬雨滴,我們不希望雨滴以完全相同的方式落下;我們希望它們從不同的位置,以稍微不同的角度,以稍微不同的速度落下。 爲了促進粒子系統所需的隨機性功能,我們使用MathHelper.h / .cpp中實現的RandF和RandUnitVec3函數:

// Returns random float in [0, 1).
static float RandF()
{
return (float)(rand()) / (float)RAND_MAX;
}
// Returns random float in [a, b).
static float RandF(float a, float b)
{
return a + RandF()*(b-a);
}
XMVECTOR MathHelper::RandUnitVec3()
{
XMVECTOR One = XMVectorSet(1.0f, 1.0f, 1.0f, 1.0f);
XMVECTOR Zero = XMVectorZero();
// Keep trying until we get a point on/in the hemisphere.
while(true)
{
// Generate random point in the cube [-1,1]^3.
XMVECTOR v = XMVectorSet(
MathHelper::RandF(-1.0f, 1.0f),
MathHelper::RandF(-1.0f, 1.0f),
MathHelper::RandF(-1.0f, 1.0f), 0.0f);
// Ignore points outside the unit sphere in order to
// get an even distribution over the unit sphere. Otherwise
// points will clump more on the sphere near the corners
// of the cube.
if(XMVector3Greater(XMVector3LengthSq(v), One))
continue;
return XMVector3Normalize(v);
}
}

以前的函數適用於C ++代碼,但我們還需要着色器代碼中的隨機數字。在着色器中生成隨機數是棘手的,因爲我們沒有着色器隨機數生成器。所以我們所做的是創建一個具有四個浮點組件的DXFI_FORMAT_R32G32B32A32_FLOAT。我們使用座標在區間[-1,1]中的隨機4D向量填充紋理。紋理將使用換行地址模式進行採樣,以便我們可以使用區間[0,1]以外的無限紋理座標。着色器代碼然後將採樣該紋理以獲得一個隨機數。有不同的方法來抽樣隨機紋理。如果每個粒子具有不同的x座標,我們可以使用x座標作爲紋理座標來獲得一個隨機數。然而,如果許多粒子具有相同的x座標,那麼它們將不會很好地工作,因爲它們都將在紋理中採樣相同的值,這不會非常隨機。另一種方法是將當前遊戲時間值用作紋理座標。這樣,不同時間產生的粒子就會得到不同的隨機值。但是,這意味着同時生成的粒子將具有相同的值。如果粒子系統需要一次發射多個粒子,這可能是一個問題。當同時生成多個粒子時,我們可以在遊戲時間中添加不同的紋理座標偏移值,以便在紋理貼圖上採樣不同的點,從而得到不同的隨機值。例如,如果我們循環20次以創建20個粒子,我們可以使用循環索引(適當縮放)來抵消用於對隨機紋理進行採樣的紋理座標。這樣,我們會得到20個不同的隨機值。

以下代碼顯示瞭如何生成隨機紋理:

ID3D11ShaderResourceView* d3dHelper::CreateRandomTexture1DSRV(
ID3D11Device* device)
{
//
// Create the random data.
// XMFLOAT4 randomValues[1024];
for(int i = 0; i < 1024; ++i)
{
randomValues[i].x = MathHelper::RandF(-1.0f, 1.0f);
randomValues[i].y = MathHelper::RandF(-1.0f, 1.0f);
randomValues[i].z = MathHelper::RandF(-1.0f, 1.0f);
randomValues[i].w = MathHelper::RandF(-1.0f, 1.0f);
}
D3D11_SUBRESOURCE_DATA initData;
initData.pSysMem = randomValues;
initData.SysMemPitch = 1024*sizeof(XMFLOAT4);
initData.SysMemSlicePitch = 0;
//
// Create the texture.
// D3D11_TEXTURE1D_DESC texDesc;
texDesc.Width = 1024;
texDesc.MipLevels = 1;
texDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
texDesc.Usage = D3D11_USAGE_IMMUTABLE;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
texDesc.ArraySize = 1;
ID3D11Texture1D* randomTex = 0;
HR(device->CreateTexture1D(&texDesc, &initData, &randomTex));
//
// Create the resource view.
// D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
viewDesc.Format = texDesc.Format;
viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE1D;
viewDesc.Texture1D.MipLevels = texDesc.MipLevels;
viewDesc.Texture1D.MostDetailedMip = 0;
ID3D11ShaderResourceView* randomTexSRV = 0;
HR(device->CreateShaderResourceView(randomTex, &viewDesc,
&randomTexSRV));
ReleaseCOM(randomTex);
return randomTexSRV;
}

請注意,對於隨機紋理,我們只需要一個mipmap級別。 爲了僅用一個mipmap對紋理進行採樣,我們使用SampleLevel內部函數。 該功能允許我們明確指定我們想要採樣的mipmap級別。 這個函數的第一個參數是採樣器; 第二個參數是紋理座標(對於1D紋理只有一個); 第三個參數是mipmap級別(在只有一個mipmap級別的紋理的情況下應爲0)。

以下着色器函數用於獲取單位球體上的隨機向量:

float3 RandUnitVec3(float offset)
{
// Use game time plus offset to sample random texture.
float u = (gGameTime + offset);
// coordinates in [-1,1]
float3 v = gRandomTex.SampleLevel(samLinear, u, 0).xyz;
// project onto unit sphere
return normalize(v);
}

20.4混合和粒子系統

粒子系統通常以某種形式的混合來繪製。 對於諸如火焰和魔法等效果,我們希望顏色強度在粒子的位置變亮。 爲此,添加劑混合效果很好。 也就是說,我們只需將源和目標顏色相加即可。 但是,顆粒通常也是透明的; 因此,我們必須通過其不透明度來縮放源粒子的顏色; 也就是說,我們使用混合參數:

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

這給出了混合等式:

C=asCsrc+Cdst

換句話說,源粒子對總和的貢獻量取決於其不透明度:粒子越不透明,貢獻的顏色就越多。 另一種方法是預先將紋理與其不透明度(由alpha通道描述)相乘,以便紋理顏色根據其不透明度進行稀釋。 然後我們使用稀釋的紋理。 在這種情況下,我們可以使用混合參數:
SrcBlend = ONE;
DestBlend = ONE;
BlendOp = ADD;

這是因爲我們基本上預先計算爲Csrc 並將其直接烘焙到紋理數據中。

添加劑的混合也有很好的效果,使得與那裏的顆粒濃度成比例的區域變亮(由於顏色的累積累積); 因此,濃度較高的區域顯得更加明亮,這通常是我們想要的(見圖20.3)。

對於像煙霧之類的東西,添加混合不起作用,因爲添加一串重疊的煙霧粒子的顏色最終會使煙霧變亮,從而使其不再變黑。與減法運算符(D3D11_BLEND_OP_REV_SUBTRACT)混合可以更好地處理煙霧,其中煙霧顆粒會從目標中減去顏色。通過這種方式,較高濃度的煙霧顆粒會導致更黑的顏色,從而產生濃煙的幻覺,而較低濃度的煙霧顆粒會導致輕微的色調,從而產生薄煙霧的幻覺。然而,雖然這對黑煙很好,但對於淺灰煙或蒸汽來說效果不佳。煙的另一種可能性是使用透明度混合,我們只是將煙霧粒子視爲半透明物體,並使用透明度混合來渲染它們。透明度混合的主要問題是將系統中的粒子相對於眼睛以前後順序進行排序。這可能是昂貴且不切實際的。由於粒子系統的隨機性,有時可以打破這條規則,而不會出現明顯的渲染錯誤。請注意,如果許多粒子系統處於場景中,則系統仍應按照從前到後的順序進行排序;我們只是不會將系統的粒子相對於彼此進行排序。請注意,使用混合時,適用§9.5.4和§9.5.5中的討論。


圖20.3 使用添加劑混合時,強度在靠近源點的地方更大,在該點更多的粒子重疊並被添加在一起。 隨着顆粒的擴散,強度減弱,因爲有更少的顆粒重疊並被加在一起。

20.5流出

我們知道GPU可以寫入紋理。例如,GPU處理寫入深度/模板緩衝區以及後臺緩衝區。 Direct3D 10中引入的功能是流出(SO)階段。 SO階段允許GPU實際將幾何結構(以頂點列表的形式)寫入綁定到流水線的SO階段的頂點緩衝區V. 具體來說,從幾何着色器輸出的頂點被寫入(或流出)到V.然後可以稍後繪製V中的幾何圖形。 圖20.4說明了這個想法(省略了鑲嵌階段)。 流出將在我們的粒子系統框架中發揮重要作用。


圖20.4 原始材料通過管道泵送。 幾何着色器輸出被流式傳輸到GPU內存中的頂點緩衝區的基元。

20.5.1爲流出創建幾何着色器

在使用流出時,必須專門創建幾何着色器。 以下代碼顯示了這是如何在效果文件中完成的:

GeometryShader gsStreamOut = ConstructGSWithSO(
CompileShader(gs_5_0, GS()),
"POSITION.xyz; VELOCITY.xyz; SIZE.xy; AGE.x; TYPE.x");
technique11 SOTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(gsStreamOut);
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}

ConstructGSWithSO的第一個參數就是編譯的幾何着色器。 第二個參數是一個字符串,它描述了正在流出的頂點的格式(即幾何着色器輸出的頂點的格式)。 在前面的例子中,頂點格式是:

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

20.5.2僅流出

在正常情況下使用流出時,幾何着色器輸出會流出到頂點緩衝區,並繼續向下到渲染管線的下一階段(光柵化)。 如果您想要一種只渲染數據並且不渲染數據的渲染技術,則必須禁用像素着色器和深度/模板緩衝區。 (禁用像素着色器和深度/模板緩衝區會禁用光柵化。)以下技術顯示瞭如何完成此操作:

DepthStencilState DisableDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
GeometryShader gsStreamOut = ConstructGSWithSO(
CompileShader(gs_5_0, StreamOutGS()),
"POSITION.xyz; VELOCITY.xyz; SIZE.xy; AGE.x; TYPE.x");
technique10 StreamOutTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, StreamOutVS()));
SetGeometryShader(gsStreamOut);
// disable pixel shader for stream-out only
SetPixelShader(NULL);
// we must also disable the depth buffer for stream-out only
SetDepthStencilState(DisableDepth, 0);
}
}

或者,您可以將空渲染目標和空深度/模板緩衝區綁定到渲染管道的輸出合併階段。

在我們的粒子系統中,我們將只使用流出技術。 該技術將僅用於創建和銷燬粒子(即更新粒子系統)。 每一幀:
1.當前粒子列表將僅用流出來繪製。 由於光柵化單元被禁用,因此這不會在屏幕上顯示任何粒子。
2.該通道的幾何着色器將基於各種條件(從粒子系統到粒子系統)創建/銷燬粒子。
3.更新的粒子列表將流式傳輸到頂點緩衝區。
然後應用程序將使用不同的渲染技術爲該幀繪製更新的粒子列表。 使用兩種技術的主要原因是幾何着色器只是做不同的事情。 對於僅流出的技術,幾何着色器輸入粒子,更新它們並輸出粒子。 對於繪圖技術,幾何着色器的任務是將點擴展爲面向相機的四邊形。 所以幾何着色器甚至不輸出相同類型的基元,因此我們需要兩個幾何着色器。

總而言之,我們需要兩種技術來在GPU上呈現我們的粒子系統:
1.僅用於更新粒子系統的流出輸出技術。
2.用於繪製粒子系統的渲染技術。

NOTE:粒子物理也可以在只輸出流的情況下逐步更新。 然而,在我們的設置中,我們有一個明確的位置函數p(t)。 所以我們不需要在流出輸出中遞增地更新粒子的位置/速度。 但是,ParticlesGS SDK示例僅使用流出輸出來更新物理,因爲它們使用不同的物理模型。

20.5.3爲流出創建一個頂點緩衝區

爲了將頂點緩衝區綁定到SO階段,以便GPU可以向其寫入頂點,必須使用D3D11_BIND_STREAM_OUTPUT綁定標誌創建頂點緩衝區。 通常,用作流輸出目標的頂點緩衝區也將被用作稍後輸入到流水線中的輸入(即,它將被綁定到IA階段,以便可以繪製內容)。 因此,我們還必須指定D3D11_BIND_VERTEX_BUFFER綁定標誌。 以下代碼片段顯示了爲Streamout用法創建頂點緩衝區的示例:

D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_DEFAULT;
vbd.ByteWidth = sizeof(Vertex) * MAX_VERTICES;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
HR(md3dDevice->CreateBuffer(&vbd, 0, &mStreamOutVB));

請注意,緩衝存儲器保持未初始化狀態。 這是因爲GPU將向其寫入頂點數據。 還要注意緩衝區的大小是有限的,所以應該注意不要流出超過最大值的頂點。

20.5.4綁定到SO階段

使用D3D11_BIND_STREAM_OUTPUT綁定標誌創建的頂點緩衝區可以綁定到流水線的SO階段,以便使用以下方法進行寫入:

void ID3D11DeviceContext::SOSetTargets(
UINT NumBuffers,
ID3D11Buffer *const *ppSOTargets,
const UINT *pOffsets);

1.NumBuffers:作爲目標綁定到SO階段的頂點緩衝區的數量。 最大值是4。
2.ppSOTargets:綁定到SO階段的頂點緩衝區數組。
3.pOffsets:偏移量數組,每個頂點緩衝區都有一個偏移量,指示SO階段應該開始寫入頂點的起始位置。

NOTE:有四個輸出插槽用於輸出。 如果少於四個緩衝區綁定到SO階段,則其他插槽將設置爲空。 例如,如果您只綁定到插槽0(第一個插槽),則插槽1,2和3將設置爲空。

20.5.5從流出階段解除綁定

在將頂點流出到頂點緩衝區之後,我們可能想繪製這些頂點定義的基元。 但是,頂點緩衝區不能綁定到SO階段並同時綁定到IA階段。 爲了從SO階段中解除一個頂點緩衝區的綁定,我們只需要在它的位置綁定一個不同的緩衝區到SO階段(可以是null)。 下面的代碼通過綁定一個空的緩衝區來從插槽0解除綁定一個頂點緩衝區:

ID3D11Buffer* bufferArray[1] = {0};
md3dDeviceContext->SOSetTargets(1, bufferArray, &offset);

20.5.6自動繪製
流出到頂點緩衝區的幾何體可以是可變的。 那麼我們繪製了多少個頂點? 幸運的是,Direct3D在內部跟蹤計數,我們可以使用ID3D11DeviceContext :: DrawAuto方法繪製寫入頂點緩衝區的幾何圖形,並使用SO:

void ID3D11DeviceContext::DrawAuto();

在調用DrawAuto之前,我們必須先將頂點緩衝區(用作流出目標)綁定到
1.輸入IA階段的插槽0。 DrawAuto方法只能在具有D3D11_BIND_STREAM_OUTPUT綁定標誌的頂點緩衝區綁定到IA階段的輸入插槽0時使用。
2.使用DrawAuto進行繪製時,我們仍然必須指定流輸出頂點緩衝區中頂點的頂點輸入佈局
3.DrawAuto不使用索引,因爲幾何着色器僅輸出由頂點列表定義的完整基元。
20.5.7乒乓頂點緩衝區
如前所述,頂點緩衝區不能綁定到OS階段,並且同時綁定到IA階段。因此,使用乒乓方案。 使用流出進行繪製時,我們使用兩個頂點緩衝區。 一個將用作輸入緩衝區,另一個將用作輸出緩衝區。 在下一個渲染幀中,兩個緩衝區的角色相反。 剛剛流入的緩衝區成爲新的輸入緩衝區,舊的輸入緩衝區成爲新的流出目標。下表顯示了使用頂點緩衝區V0,V1 的三次迭代。

\documentclass{article}\begin{document}\begin{tabular}{|l|c|r|} %l(left)居左顯示 r(right)居右顯示 c居中顯示\hline &輸入頂點緩衝區綁定到IA階段&輸出頂點緩衝區綁定到SO階段\\\hline  第i幀&V_0&V_1\\\hline 第i+1幀&V_1&V_0\\\hline 第i+2幀&V_0&V_1\\\hline \end{tabular}\end{document}

20.6基於GPU的粒子系統

粒子系統通常隨着時間發射並破壞粒子。 看似自然的做法是使用動態頂點緩衝區並跟蹤CPU上的粒子和粒子。 然後,頂點緩衝區將填充當前活動的粒子並繪製。 但是,從前一節我們知道,單獨的流出輸出通道可以完全在GPU上處理此產卵/終止更新週期。 這樣做的動機是效率 - 每當CPU需要將數據上傳到GPU時會產生一些開銷; 此外,它將工作從CPU轉移到GPU,從而將CPU釋放出來用於AI或物理等其他任務。 在本節中,我們將解釋我們的粒子系統框架的一般細節。

20.6.1粒子效應

關於特定粒子系統如何表現的具體細節在效果文件中實現。也就是說,我們將爲每個粒子系統(例如,雨,火,煙等)製作一個不同的(但相似的)效果文件。粒子如何發射,破壞和繪製的細節都在相應的效果文件中編寫腳本,因爲它們因系統而異。例子:
1.我們可能會在撞擊地面時摧毀一個雨滴,而火焰粒子會在幾秒鐘之後被摧毀。
2.煙霧顆粒可能會隨着時間消逝,而雨滴顆粒則不會。同樣,煙霧顆粒的大小可能會隨着時間而擴大,而雨滴顆粒則不會。
3.線性原語通常適用於雨水建模,而廣告牌四元素則用於火/煙粒子。通過對不同的粒子系統使用不同的效果,我們可以讓幾何着色器將點展開爲線條以表示下雨,並指向火/煙的四邊形。
4.雨和煙霧顆粒的初始位置和速度顯然不同。

重申一下,這些粒子系統的具體細節可以在效果文件中實現,因爲在我們的系統中,着色器代碼處理粒子的創建,銷燬和更新。 這種設計非常方便,因爲要添加一個新的粒子系統,我們只需編寫一個描述其行爲的新效果文件。
20.6.2粒子系統類
下面顯示的類處理用於創建,更新和繪製粒子系統的C ++相關代碼。 這個代碼是一般的,並將應用於我們創建的所有粒子系統。

class ParticleSystem
{
public:
ParticleSystem();
~ParticleSystem();
// Time elapsed since the system was reset.
float GetAge()const;
void SetEyePos(const XMFLOAT3& eyePosW);
void SetEmitPos(const XMFLOAT3& emitPosW);
void SetEmitDir(const XMFLOAT3& emitDirW);
void Init(ID3D11Device* device, ParticleEffect* fx,
ID3D11ShaderResourceView* texArraySRV,
ID3D11ShaderResourceView* randomTexSRV,
UINT maxParticles);
void Reset();
void Update(float dt, float gameTime);
void Draw(ID3D11DeviceContext* dc, const Camera& cam);
private:
void BuildVB(ID3D11Device* device);
ParticleSystem(const ParticleSystem& rhs);
ParticleSystem& operator=(const ParticleSystem& rhs);
private:
UINT mMaxParticles;
bool mFirstRun;
float mGameTime;
float mTimeStep;
float mAge;
XMFLOAT3 mEyePosW;
XMFLOAT3 mEmitPosW;
XMFLOAT3 mEmitDirW;
ParticleEffect* mFX;
ID3D11Buffer* mInitVB;
ID3D11Buffer* mDrawVB;
ID3D11Buffer* mStreamOutVB;
ID3D11ShaderResourceView* mTexArraySRV;
ID3D11ShaderResourceView* mRandomTexSRV;
};

除了繪製方法之外,現在粒子系統類方法的實現是相當常規的(例如,創建頂點緩衝區)。 因此,我們不會在書中顯示它們(請參閱相應章節的源代碼)。

NOTE:粒子系統使用紋理數組來紋理粒子。 這個想法是我們可能不希望所有的粒子看起來完全一樣。 例如,爲了實現煙霧粒子系統,我們可能想要使用幾種煙霧紋理來添加一些變化; 原始ID可以在像素着色器中用於在紋理數組中的煙霧紋理之間交替。

20.6.3發射粒子

由於幾何着色器負責創建/銷燬粒子,因此我們需要有特殊的發射器粒子。發射粒子可能會或可能不會被繪製。例如,如果您希望發射器粒子不可見,那麼只需要粒子繪製幾何着色器不輸出它們。發射體粒子在系統中的行爲與其他粒子的行爲不同,因爲它們可以產生其他粒子。例如,發射器粒子可以跟蹤已經經過了多少時間,並且當該時間到達某個點時,它發射新的粒子。這樣,隨着時間的推移會產生新的粒子。我們使用頂點::粒子結構的類型成員來指示發射器粒子。而且,通過限制哪些粒子被允許發射其他粒子,它使我們能夠控制粒子如何發射。例如,通過只有一個發射器粒子,很容易控制每幀會創建多少個粒子。只有流的幾何着色器應始終輸出至少一個發射器粒子,因爲如果粒子系統丟失其所有發射器,它就會有效地死亡;然而,對於一些粒子系統來說,這可能是期望的結果。

我們在本書中演示的粒子系統在粒子系統的生命週期中僅使用一個發射器粒子。但是,如果需要,可以擴展粒子系統框架以使用更多。而且,其他顆粒也可以發射顆粒。例如,SDK的ParticlesGS演示包含一個啓動粒子(一種產生外殼粒子的不可見粒子),外殼粒子(未爆炸的煙花,在一段時間後會爆炸成新的餘燼粒子),其中一些餘燼也會爆炸成新的二次餘燼粒子,造成二次爆炸。在這個意義上,發射器粒子可以發射其他發射器粒子。練習1要求你探索這個。

20.6.4初始化頂點緩衝區

在我們的粒子系統中,我們有一個特殊的初始化頂點緩衝區。這個頂點緩衝區只包含一個發射器粒子。我們首先繪製這個頂點緩衝區來啓動粒子系統。這個發射器粒子將在隨後的幀中開始產生其他粒子。請注意,初始化頂點緩衝區只繪製一次(系統復位時除外)。在用單個發射器粒子初始化粒子系統之後,我們以乒乓方式使用兩個流輸出頂點緩衝區。

如果我們需要從頭開始重新啓動系統,則初始化頂點緩衝區也很有用。 我們可以使用這段代碼重新啓動粒子系統:

void ParticleSystem::Reset()
{
mFirstRun = true;
mAge = 0.0f;
}

將mFirstRun設置爲true將指示粒子系統在下一次繪製調用時繪製初始化頂點緩衝區,從而使用單個發射器粒子重新啓動粒子系統。

20.6.5更新/繪圖方法

回想一下,我們需要兩種技術來在GPU上呈現我們的粒子系統:
1.僅用於更新粒子系統的流出輸出技術。
2.用於繪製粒子系統的渲染技術。
下面的代碼是這樣做的,除了處理兩個頂點緩衝區的乒乓方案:

void ParticleSystem::Draw(ID3D11DeviceContext* dc, const Camera& cam)
{
XMMATRIX VP = cam.ViewProj();
//
// Set constants.
//m FX->SetViewProj(VP);
mFX->SetGameTime(mGameTime);
mFX->SetTimeStep(mTimeStep);
mFX->SetEyePosW(mEyePosW);
mFX->SetEmitPosW(mEmitPosW);
mFX->SetEmitDirW(mEmitDirW);
mFX->SetTexArray(mTexArraySRV);
mFX->SetRandomTex(mRandomTexSRV);
//
// Set IA stage.
//
dc->IASetInputLayout(InputLayouts::Particle);
dc->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
UINT stride = sizeof(Vertex::Particle);
UINT offset = 0;
// On the first pass, use the initialization VB. Otherwise, use
// the VB that contains the current particle list.
if( mFirstRun )
dc->IASetVertexBuffers(0, 1, &mInitVB, &stride, &offset);
else
dc->IASetVertexBuffers(0, 1, &mDrawVB, &stride, &offset);
//
// Draw the current particle list using stream-out only to update them.
// The updated vertices are streamed-out to the target VB.
//
dc->SOSetTargets(1, &mStreamOutVB, &offset);
D3DX11_TECHNIQUE_DESC techDesc;
mFX->StreamOutTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mFX->StreamOutTech->GetPassByIndex( p )->Apply(0, dc);
if(mFirstRun)
{
dc->Draw(1, 0);
mFirstRun = false;
}
else
{
dc->DrawAuto();
}
}
// done streaming-out--unbind the vertex buffer
ID3D11Buffer* bufferArray[1] = {0};
dc->SOSetTargets(1, bufferArray, &offset);
// ping-pong the vertex buffers
std::swap(mDrawVB, mStreamOutVB);
//
// Draw the updated particle system we just streamed-out.
//
dc->IASetVertexBuffers(0, 1, &mDrawVB, &stride, &offset);
mFX->DrawTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mFX->DrawTech->GetPassByIndex(p)->Apply(0, dc);
dc->DrawAuto();
}
}


圖20.5 粒子系統演示屏幕截圖顯示火災。

20.7 火

以下是渲染火焰粒子系統的效果。 它由兩種技術組成:
1.僅用於更新粒子系統的流出輸出技術。
2.用於繪製粒子系統的渲染技術。
編程到這兩種技術中的邏輯通常會因粒子系統而不同,因爲銷燬/產卵/渲染規則將會不同。 火焰粒子在發射位置發射,但被賦予隨機的初始速度以傳播火焰以產生火球。

//***********************************************
// GLOBALS *
//***********************************************
cbuffer cbPerFrame
{
float3 gEyePosW;
// for when the emit position/direction is varying
float3 gEmitPosW;
float3 gEmitDirW;
float gGameTime;
float gTimeStep;
float4x4 gViewProj;
};
cbuffer cbFixed
{
// Net constant acceleration used to accerlate the particles.
float3 gAccelW = {0.0f, 7.8f, 0.0f};
// Texture coordinates used to stretch texture over quad
// when we expand point particle into a quad.
float2 gQuadTexC[4] =
{
float2(0.0f, 1.0f),
float2(1.0f, 1.0f),
float2(0.0f, 0.0f),
float2(1.0f, 0.0f)
};
};
// Array of textures for texturing the particles.
Texture2DArray gTexArray;
// Random texture used to generate random numbers in shaders.
Texture1D gRandomTex;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
DepthStencilState DisableDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
DepthStencilState NoDepthWrites
{
DepthEnable = TRUE;
DepthWriteMask = ZERO;
};
BlendState AdditiveBlending
{
AlphaToCoverageEnable = FALSE;
BlendEnable[0] = TRUE;
SrcBlend = SRC_ALPHA;
DestBlend = ONE;
BlendOp = ADD;
SrcBlendAlpha = ZERO;
DestBlendAlpha = ZERO;
BlendOpAlpha = ADD;
RenderTargetWriteMask[0] = 0x0F;
};
//***********************************************
// HELPER FUNCTIONS *
//***********************************************
float3 RandUnitVec3(float offset)
{
// Use game time plus offset to sample random texture.
float u = (gGameTime + offset);
// coordinates in [-1,1]
float3 v = gRandomTex.SampleLevel(samLinear, u, 0).xyz;
// project onto unit sphere
return normalize(v);
}
//***********************************************
// STREAM-OUT TECH *
//***********************************************
#define PT_EMITTER 0
#define PT_FLARE 1
struct Particle
{
float3 InitialPosW : POSITION;
float3 InitialVelW : VELOCITY;
float2 SizeW : SIZE;
float Age : AGE;
uint Type : TYPE;
};
Particle StreamOutVS(Particle vin)
{
return vin;
}
// The stream-out GS is just responsible for emitting
// new particles and destroying old particles. The logic
// programed here will generally vary from particle system
// to particle system, as the destroy/spawn rules will be
// different.
[maxvertexcount(2)]
void StreamOutGS(point Particle gin[1],
inout PointStream<Particle> ptStream)
{
gin[0].Age += gTimeStep;
if(gin[0].Type == PT_EMITTER)
{
// time to emit a new particle?
if(gin[0].Age > 0.005f)
{
float3 vRandom = RandUnitVec3(0.0f);
vRandom.x *= 0.5f;
vRandom.z *= 0.5f;
Particle p;
p.InitialPosW = gEmitPosW.xyz;
p.InitialVelW = 4.0f*vRandom;
p.SizeW = float2(3.0f, 3.0f);
p.Age = 0.0f;
p.Type = PT_FLARE;
ptStream.Append(p);
// reset the time to emit
gin[0].Age = 0.0f;
}
// always keep emitters
ptStream.Append(gin[0]);
}
else
{
// Specify conditions to keep particle; this may vary
// from system to system.
if(gin[0].Age <= 1.0f)
ptStream.Append(gin[0]);
}
}
GeometryShader gsStreamOut = ConstructGSWithSO(
CompileShader(gs_5_0, StreamOutGS()),
"POSITION.xyz; VELOCITY.xyz; SIZE.xy; AGE.x; TYPE.x");
technique11 StreamOutTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, StreamOutVS()));
SetGeometryShader(gsStreamOut);
// disable pixel shader for stream-out only
SetPixelShader(NULL);
// we must also disable the depth buffer for stream-out only
SetDepthStencilState(DisableDepth, 0);
}
}
//***********************************************
// DRAW TECH *
//***********************************************
struct VertexOut
{
float3 PosW : POSITION;
float2 SizeW : SIZE;
float4 Color : COLOR;
uint Type : TYPE;
};
VertexOut DrawVS(Particle vin)
{
VertexOut vout;
float t = vin.Age;
// constant acceleration equation
vout.PosW = 0.5f*t*t*gAccelW + t*vin.InitialVelW + vin.InitialPosW;
// fade color with time
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;
}
struct GeoOut
{
float4 PosH : SV_Position;
float4 Color : COLOR;
float2 Tex : TEXCOORD;
};
// The draw GS just expands points into camera facing quads.
[maxvertexcount(4)]
void DrawGS(point VertexOut gin[1],
inout TriangleStream<GeoOut> triStream)
{
// do not draw emitter particles.
if(gin[0].Type != PT_EMITTER)
{
//
// Compute world matrix so that billboard faces the camera.
//
float3 look = normalize(gEyePosW.xyz - gin[0].PosW);
float3 right = normalize(cross(float3(0,1,0), look));
float3 up = cross(look, right);
//
// Compute triangle strip vertices (quad) in world space.
//
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);
//
// Transform quad vertices to world space and output
// them as a triangle strip.
// GeoOut gout;
[unroll]
for(int i = 0; i < 4; ++i)
{
gout.PosH = mul(v[i], gViewProj);
gout.Tex = gQuadTexC[i];
gout.Color = gin[0].Color;
triStream.Append(gout);
}
}
}
float4 DrawPS(GeoOut pin) : SV_TARGET
{
return gTexArray.Sample(samLinear, float3(pin.Tex, 0))*pin.Color;
}
technique11 DrawTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, DrawVS()));
SetGeometryShader(CompileShader(gs_5_0, DrawGS()));
SetPixelShader(CompileShader(ps_5_0, DrawPS()));
SetBlendState(AdditiveBlending, float4(0.0f, 0.0f, 0.0f, 0.0f), 0xffffffff);
SetDepthStencilState(NoDepthWrites, 0);
}
}

20.8 雨

我們還實施了雨水粒子系統。 降雨粒子系統的行爲由降雨效應(rain.fx)指定。 它遵循與fire.fx類似的模式,但銷燬/產卵/渲染規則不同。 例如,我們的雨滴以微小的角度向下加速,而火焰粒子向上加速。 此外,降雨粒子擴展成線而不是四邊形(見圖20.6)。 降雨粒子在相機上方的隨機位置發射; 雨總是“跟隨”相機,這樣我們就不必在世界各地發出雨滴。 也就是說,只要在攝像機附近放射雨粒就足以讓人感覺下雨。 請注意,雨水系統不使用任何混合。


圖20.6 粒子系統演示顯示下雨的截圖。

//***********************************************
// GLOBALS *
//***********************************************
cbuffer cbPerFrame
{
float3 gEyePosW;
// for when the emit position/direction is varying
float3 gEmitPosW;
float3 gEmitDirW;
float gGameTime;
float gTimeStep;
float4x4 gViewProj;
};
cbuffer cbFixed
{
// Net constant acceleration used to accerlate the particles.
float3 gAccelW = {-1.0f, -9.8f, 0.0f};
};
// Array of textures for texturing the particles.
Texture2DArray gTexArray;
// Random texture used to generate random numbers in shaders.
Texture1D gRandomTex;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
DepthStencilState DisableDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
DepthStencilState NoDepthWrites
{
DepthEnable = TRUE;
DepthWriteMask = ZERO;
};
//***********************************************
// HELPER FUNCTIONS *
//***********************************************
float3 RandUnitVec3(float offset)
{
// Use game time plus offset to sample random texture.
float u = (gGameTime + offset);
// coordinates in [-1,1]
float3 v = gRandomTex.SampleLevel(samLinear, u, 0).xyz;
// project onto unit sphere
return normalize(v);
}
float3 RandVec3(float offset)
{
// Use game time plus offset to sample random texture.
float u = (gGameTime + offset);
// coordinates in [-1,1]
float3 v = gRandomTex.SampleLevel(samLinear, u, 0).xyz;
return v;
}
//***********************************************
// STREAM-OUT TECH *
//***********************************************
#define PT_EMITTER 0
#define PT_FLARE 1
struct Particle
{
float3 InitialPosW : POSITION;
float3 InitialVelW : VELOCITY;
float2 SizeW : SIZE;
float Age : AGE;
uint Type : TYPE;
};
Particle StreamOutVS(Particle vin)
{
return vin;
}
// The stream-out GS is just responsible for emitting
// new particles and destroying old particles. The logic
// programed here will generally vary from particle system
// to particle system, as the destroy/spawn rules will be
// different.
[maxvertexcount(6)]
void StreamOutGS(point Particle gin[1],
inout PointStream<Particle> ptStream)
{
gin[0].Age += gTimeStep;
if(gin[0].Type == PT_EMITTER)
{
// time to emit a new particle?
if(gin[0].Age > 0.002f)
{
for(int i = 0; i < 5; ++i)
{
// Spread rain drops out above the camera.
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 = PT_FLARE;
ptStream.Append(p);
}
// reset the time to emit
gin[0].Age = 0.0f;
}
// always keep emitters
ptStream.Append(gin[0]);
}
else
{
// Specify conditions to keep particle; this may vary
// from system to system.
if( gin[0].Age <= 3.0f )
ptStream.Append(gin[0]);
}
}
GeometryShader gsStreamOut = ConstructGSWithSO(
CompileShader(gs_5_0, StreamOutGS()),
"POSITION.xyz; VELOCITY.xyz; SIZE.xy; AGE.x; TYPE.x");
technique11 StreamOutTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, StreamOutVS()));
SetGeometryShader(gsStreamOut);
// disable pixel shader for stream-out only
SetPixelShader(NULL);
// we must also disable the depth buffer for stream-out only
SetDepthStencilState(DisableDepth, 0);
}
}
//***********************************************
// DRAW TECH *
//***********************************************
struct VertexOut
{
float3 PosW : POSITION;
uint Type : TYPE;
};
VertexOut DrawVS(Particle vin)
{
VertexOut vout;
float t = vin.Age;
// constant acceleration equation
vout.PosW = 0.5f*t*t*gAccelW + t*vin.InitialVelW + vin.InitialPosW;
vout.Type = vin.Type;
return vout;
}
struct GeoOut
{
float4 PosH : SV_Position;
float2 Tex : TEXCOORD;
};
// The draw GS just expands points into lines.
[maxvertexcount(2)]
void DrawGS(point VertexOut gin[1],
inout LineStream<GeoOut> lineStream)
{
// do not draw emitter particles.
if(gin[0].Type != PT_EMITTER)
{
// Slant line in acceleration direction.
float3 p0 = gin[0].PosW;
float3 p1 = gin[0].PosW + 0.07f*gAccelW;
GeoOut v0;
v0.PosH = mul(float4(p0, 1.0f), gViewProj);
v0.Tex = float2(0.0f, 0.0f);
lineStream.Append(v0);
GeoOut v1;
v1.PosH = mul(float4(p1, 1.0f), gViewProj);
v1.Tex = float2(1.0f, 1.0f);
lineStream.Append(v1);
}
}
float4 DrawPS(GeoOut pin) : SV_TARGET
{
return gTexArray.Sample(samLinear, float3(pin.Tex, 0));
}
technique11 DrawTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, DrawVS()));
SetGeometryShader(CompileShader(gs_5_0, DrawGS()));
SetPixelShader(CompileShader(ps_5_0, DrawPS()));
SetDepthStencilState(NoDepthWrites, 0);
}
}

20.9 總結

1.粒子系統是一組粒子(通常很小),它們的行爲都相似,但有點隨機。粒子系統可用於模擬各種各樣的現象,如火災,雨水,煙霧,爆炸,灑水和魔法效應。
2.我們用點來模擬我們的粒子,然後在渲染之前將它們展開成幾何着色器中面向相機的四邊形。這意味着我們可以獲得使用點的效率:較小的內存佔用量,我們只需應用物理到一個頂點而不是四個四邊形頂點,而且,通過稍後將點擴展到四邊形,我們還可以獲得具有不同大小的粒子並將紋理映射到它們的能力。請注意,沒有必要將點擴展到四邊形。例如,線條可以用來很好地模擬雨滴;我們可以使用不同的幾何圖形將點展開爲線條。
3.恆定加速度的粒子軌跡由式:p(t)=12t2a+tv0+p0 給出.其中a是恆定的加速度矢量,v0 是粒子的初始速度(即時間t = 0時的速度),p0 是粒子(即時間t = 0時的位置)。通過這個方程,我們可以通過評估t處的函數得到任何時刻t≥0時的粒子位置。
4.當您希望粒子系統的強度與粒子密度成比例時,請使用添加劑混合。對透明粒子使用透明度混合。不按照先後次序對透明粒子系統進行排序可能是也可能不是問題(即問題可能會或可能不明顯)。通常對於粒子系統,深度寫入是禁用的,因此粒子不會互相混淆。但是,深度測試仍然可用,因此非粒子對象不會遮擋粒子。
5.流出(SO)階段允許GPU將幾何形狀(以頂點列表的形式)寫入綁定到管線的SO階段的頂點緩衝區V.具體來說,從幾何着色器輸出的頂點被寫入(或流出)到V.然後可以稍後繪製V中的幾何圖形。我們使用這種流出功能來實現完全在GPU上運行的粒子系統。要做到這一點,我們使用兩種技術:
a)僅用於更新粒子系統的流出輸出技術。 在這個渲染過程中,幾何着色器根據粒子系統和粒子系統之間的各種條件生成/銷燬/更新粒子。 生物粒子然後被流出到頂點緩衝區。
b)用於繪製粒子系統的渲染技術。 在這個渲染過程中,我們繪製了流出的活動粒子。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章