DirectX11——粒子系統
DirectX11 粒子系統
前言
粒子系統表示三維計算機圖形學中模擬一些特定的模糊現象的技術,而這些現象用其它傳統的渲染技術難以實現真實感的物理運動規律。(百度百科)
我們經常使用粒子系統來模擬火焰、爆炸、煙霧等現象,在Unity3D中就有一套成熟粒子系統以供遊戲開發者使用。最近在學習使用DirectX11中的著色器反射來構建粒子系統渲染框架,並嘗試使用這一套框架實現爆炸和噴泉特效。
本人學習的DirectX11是基於Windows SDK實現的,本次粒子系統的所有前置知識以及粒子系統的實現均來自DirectX11 with Windows SDK教程:https://www.cnblogs.com/X-Jun/p/9028764.html
本次粒子系統的框架也使用了https://github.com/MKXJun/DirectX11-With-Windows-SDK中第35個專案中的ParticleEffect框架和ParticleRender。
涵蓋知識
粒子系統的實現屬於教程中高階部分的內容,需要的知識比較全面,具體請看思維導圖:
總結一下:著色器的書寫與C++中建立著色器、流輸出階段、利用著色器反射修改緩衝區資料、混合狀態和深度/模板測試、利用幾何著色器實現公告板效果。
通過粒子系統的實現學習,可以學習到:
-
利用幾何著色器和流輸出階段高效產生和儲存粒子
-
使用物理知識來模擬物理運動規律
-
鍛鍊設計渲染框架的能力
粒子系統
粒子系統需要較多的屬性,不同的粒子系統會擁有不同的屬性,在通用的粒子系統中,我們定義以下屬性:
1、發射位置
2、發射方向
3、發射間隔
4、粒子存活時間
5、系統時長
6、系統步長
7、最大粒子數目
8、觀察位置和觀察矩陣
這些屬性應該不難理解,觀察位置和觀察矩陣在其他渲染系統中也是必備的。
粒子
粒子本身也有不少的屬性,通用的粒子屬性如下:
struct ParticleVertex { XMFLOAT3 initialPos; // 初始位置 XMFLOAT3 initialVel; // 初始速度 XMFLOAT3 size; // 粒子大小 floatage; // 粒子年齡 uint type; // 粒子型別 }
粒子型別包括發射器粒子和普通發射粒子,發射器粒子會根據自身年齡(解釋為年齡不太合適,因為它會不斷地迴圈置零)來判斷是否發射新粒子,發射新粒子後重新積累時間,又一次達到系統的發射間隔時發射新粒子。在幾何著色器中,發射粒子總是被輸出到頂點緩衝區,如果沒控制好粒子最大數量、粒子發射間隔和粒子存活時間,緩衝區無法發容納射器粒子,粒子系統就會消失(沒有粒子)。
隨機數值
我們可以在C++使用random
來產生隨機數,但在HLSL中並沒有產生隨機數的方法,因此我們需要自己定義一些產生隨機數的方法。通常情況下,對一個Texture1D
資源進行不同位置的取樣可以達到獲得隨機數的效果。建立一個1D紋理,裡面每個元素是float4
(DXGI_FORMAT_R32G32B32A32_FLOAT
),然後我們使用區間[-1, 1]
的隨機4D向量來填滿紋理,取樣的時候則使用wrap定址模式即可。取樣的結果也是在[-1, 1]
,可根據需要通過計算使之落在指定範圍。
下面是獲得隨機單位向量的方法:
float3 RandUnitVec3(float offset) { // 使用遊戲時間加上偏移值來從隨機紋理取樣 float u = (g_GameTime + offset); // 分量均在[-1,1] float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz; // 標準化向量 return normalize(v); }
接下來的爆炸和噴泉特效的簡單模擬,在這裡只講解主要的步驟。
噴泉
噴泉系統:粒子從某個點產生,並沿著圓錐體範圍內的隨機方向向上發射,最終重力會使得它們掉落到地面。
重力的模擬比較簡單,在Fountain.hlsli
中定義重力加速度G
,在計算粒子位置時使用物理勻加速直線運動位移公式:x = 1/2 * a * t * t + v * t
計算位移,加上初始位置即可。
// Fountain.hlsli ... cbuffer CBFixed : register(b1) { // 重力加速度 float3 g_G = float3(0.0f, -9.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) }; } ... // Fountain_VS.hlsl VertexOut VS(VertexParticle vIn) { VertexOut vOut; float t = vIn.Age; // 恆定加速度等式 vOut.PosW = 0.5f * t * t * g_G + 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; }
在隨機數部分已經提及,我們定義了產生隨機單位向量的方法:
float3 RandUnitVec3(float offset) { // 使用遊戲時間加上偏移值來從隨機紋理取樣 float u = (g_GameTime + offset); // 分量均在[-1,1] float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz; // 標準化向量 return normalize(v); }
在產生新粒子我們可以使用以下計算使向量集中在圓錐區域:
float3 vRandom = RandUnitVec3(offset); vRandom.x *= 0.5f; // 可根據圓錐的範圍乘上特定值使向量集中在指定區域 vRandom.z *= 0.5f; vRandom.y = sqrt(1 - vRandom.x * vRandom.x - vRandom.z * vRandom.z); // 落在單位圓上
在模擬噴泉中發現,一幀發射一個粒子的效果仍然很差,需要使用迴圈來產生更多的粒子。
注意:幾何著色器的每次呼叫最多隻能處理1024個標量
// Fountain_SO_GS.hlsl #include " Fountain.hlsli" [maxvertexcount(4)] 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) { // for迴圈產生更多粒子,以三個粒子為例 for (int i = 0; i < 3; i++) { float3 vRandom = RandUnitVec3(g_GameTime * i); // 不同的偏移量進行採集 vRandom.x *= 0.5f; // 可根據圓錐的範圍乘上特定值使向量集中在指定區域 vRandom.z *= 0.5f; vRandom.y = sqrt(1 - vRandom.x * vRandom.x - vRandom.z * vRandom.z);// 落在單位圓上 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]); } }
爆炸
發射器粒子產生N個隨機方向的外殼粒子。在經過一個短暫時間後,每個外殼粒子應當爆炸產生M個粒子。每個外殼不需要在同一個時間發生爆炸——通過隨機性賦上不同的爆炸倒計時。
在這裡為了方便觀察將N和M設為10,儲存發射器粒子觀察隨機性。
與噴泉相比爆炸系統更加複雜,發射器會發射兩種粒子,故我們需要新增第三種粒子:外殼粒子PT_SHELL
。外殼粒子會隨機時間爆炸,故我們需要獲得一個隨機數來定義爆炸時間。我們先定義一個方法來獲得[-1, 1]
間的隨機數。
float RandNum(float offset) { // 使用遊戲時間加上偏移值來從隨機紋理取樣 float u = (g_GameTime + offset); // 在[-1,1] float v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).x; return v; }
再通過以下計算使其落在粒子的最大存活時間內:
float Randomtime = (RandNum(i * g_GameTime) + 1) * 0.5f * g_AliveTime;
外殼粒子的初始年齡(Age)就是粒子存活時間減去獲得的隨機爆炸時間:
p.Age = g_AliveTime - Randomtime;
這樣就可以在外殼粒子生命週期結束時產生新的普通粒子。
完整的Explosion_SO_GS
如下:
#include "Explosion.hlsli" [maxvertexcount(11)] 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) { uint ShellParticleCount = 10; [unroll] for (int i = 0; i < ShellParticleCount; i++) { float3 vRandom = RandUnitVec3(i * g_GameTime); float Randomtime = (RandNum(i * g_GameTime) + 1) * 0.5f * g_AliveTime; VertexParticle p; p.InitialPosW = g_EmitPosW.xyz; p.InitialVelW = 5.0f * vRandom; p.SizeW = float2(2.0f, 2.0f); p.Age = g_AliveTime - Randomtime; p.Type = PT_SHELL; output.Append(p); } // 重置時間準備下一次發射 gIn[0].Age = 0.0f; } // 總是保留髮射器 output.Append(gIn[0]); } else if (gIn[0].Type == PT_SHELL) { if (gIn[0].Age > g_AliveTime) { uint FlareParticleCount = 10; [unroll] for (int i = 0; i < FlareParticleCount; i++) { float3 vRandom = RandUnitVec3(i * g_GameTime * 0.9f); VertexParticle p; p.InitialPosW = gIn[0].InitialPosW; p.InitialVelW = 10.0f * vRandom; p.SizeW = float2(1.0f, 1.0f); p.Age = 0; p.Type = PT_FLARE; output.Append(p); } } else { gIn[0].InitialPosW = gIn[0].InitialPosW + g_TimeStep * gIn[0].InitialVelW; gIn[0].InitialVelW = gIn[0].InitialVelW * (1 + g_AccelW * g_TimeStep); output.Append(gIn[0]); } } else { // 用於限制粒子數目產生的特定條件,對於不同的粒子系統限制也有所變化 if (gIn[0].Age <= g_AliveTime) { gIn[0].InitialPosW = gIn[0].InitialPosW + g_TimeStep * gIn[0].InitialVelW; gIn[0].InitialVelW = gIn[0].InitialVelW * (1 + g_AccelW * g_TimeStep); output.Append(gIn[0]); } } }
在位置和速度計算中,g_AccelW
是粒子運動中受到的空氣阻力產生的加速度,忽略重力影響。
演示
下面的動圖演示噴泉和爆炸效果: