天天看點

特效的批量繪制

特效的批量繪制

張嘉華([email protected])

特效系統是遊戲中的一個重要組成部分,在場景布谷,角色技能等有廣泛應用。一個特效往往包含多種多樣的組成元素:粒子系統,公告闆,音效,圖元軌迹,模型特效,鏡頭濾鏡/震動/模糊,動态光源等。其中以粒子系統和公告闆在場景中用得最多,往往一個特效由上述這些元素構成多個軌道,沿着時間軸以不同起始時間(相位)和周期播放。批量繪制和幾何執行個體化(Batching and Geometry Instancing)相信讀者已經不太陌生,那麼比較困難的是既要滿足美術開發時每個執行個體的多樣性靈活性(多種渲染狀态同時存在),又要保留批量繪制的高效(不同狀态,不同成分的執行個體一起繪制)。根據我們的實驗,在一個場景同時繪制100多個特效,每個特效包含4~10個粒子系統軌道,2~6個公告闆軌道能夠有好的效果和效率。接下來本文以粒子系統和公共闆為例簡單介紹一下我引擎中的實作方式。

特效的批量繪制

1. 多個粒子系統的批量繪制

單個粒子系統的實作主要有狀态保持和非狀态保持兩類。狀态保持就是粒子系統中的每個粒子每幀都進行更新,根據各樣的規則分别更新加速度,速度,位移,這種方式能夠讓美術在周期内對加速度或速度的變化根據曲線或者規則變化,也能夠在周期中中途臨時改變粒子的走向等,但是這種方式往往需要用若幹RenderTarget紋理儲存粒子目前位移,速度等狀态,通過渲染到紋理來每幀更新紋理中每個粒子的這些狀态;另外一種方式就是非狀态保持,粒子的狀态在每幀由公式根據平均加速度,最大速度,最小速度計算出目前的位置,這種方式比較利于在GPU的Vertex Shader中為每個粒子直接用中學實體公式:Pt=P0+vt+0.5*a*t*t計算出位置,而不用像狀态保持方式那樣通過Shader Model 3.0支援的tex2DLod這樣的指令讀取上一幀的狀态。在我的遊戲中,由于臨時改變狀态的行為比較少,是以單個粒子系統的實作隻在GPU中采用了比較簡單和易于實作的非狀态保持方式。

1.1 多個粒子系統批量繪制的需求

無論狀态保持和非狀态保持,對于有經驗的3D程式員來說,實作都不會太困難,而比較困難的是跨系統的粒子之間如何也進行批量繪制。那麼接下來研究下多個粒子系統批量繪制的需求,也就是看看粒子系統之間究竟有多少差異需要進行提取和合并:

1, 不同的粒子系統采用不同的貼圖,貼圖需要合并

2, 不同的粒子系統下面這些參數可能不一樣:最小速度,最大速度,最小角速度,最大角速度,最小生命周期,最大生命周期,每次發射粒子數,粒子發射間距等

3, 粒子系統跟幀緩沖的混合模式不一樣:0暗的疊加(srcblend= srcalpha, destblend=invsrcalpha),1亮的疊加(srcblend=srcalpha,destblend=one)

4, 粒子的朝向不一樣:0朝向某個法線,1朝向鏡頭

5, 粒子的開始相位和随機數生成不一樣

1.2 Constants Instancing

根據這些需求,我們開始進行合并和批量繪制,采用的方法是GPU Gems2 Chapter3裡面提到的四種執行個體方法的第三種:Constants Instancing

(http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter03.html)。既提供一些對不支援硬體Instancing(不支援SetStreamSourceFreq)的向後相容,也提供了能夠每幀通過Constants設定粒子系統參數的靈活性。

1.3 頂點緩沖和索引緩沖的建立填充

首先,我們需要建立足夠存儲多個粒子系統的所有粒子的頂點緩沖和索引緩沖:

ret=pd3dDevice->CreateVertexBuffer(MAX_PARTICLE_SYSTEM_BATCH*MAX_PARTICLE_NUMBER*4*sizeof(PNCT1Vertex),0, D3DFVF_PNCT1Vertex,D3DPOOL_MANAGED, &g_pVB, NULL );

ret=pd3dDevice->CreateIndexBuffer(MAX_PARTICLE_SYSTEM_BATCH*MAX_PARTICLE_NUMBER*6*sizeof(int),0,D3DFMT_INDEX32,D3DPOOL_MANAGED,&g_pIB,NULL);

MAX_PARTICLE_SYSTEM_BATCH是定義每批次繪制的最大粒子系統數量的宏

MAX_PARTICLE_NUMBER是每個粒子系統中最大的粒子數量

所有批次的粒子系統都是共用這個頂點緩沖和索引緩沖,接下來就是填充頂點和索引資料,這裡每個頂點的position存的是構成每個粒子的四邊形面片的每個頂點在對象空間中的坐标,Shader的語義是POSITION0;每個頂點的索引index的三個分量存的是i粒子系統序号,j單個粒子系統中粒子序号和頂點在粒子面片4個頂點中的序号,語義是BLENDINDICES;每個頂點的uv存放的是紋理坐标,這裡減少了0.01f是為了在合并後的紋理采樣時看不到紋理間的裂縫。

int vertexcount=0;

//設定頂點

PNCT1Vertex* pVertices=NULL;

ret=g_pVB->Lock(0,0,(void**)&pVertices,0);

vertexcount=0;

for(int i=0;i

{

for(int j=0;j

{

pVertices[vertexcount+0].position=float3(-1.0f,-1.0f,0.0f);

pVertices[vertexcount+0].index=int3(i, j,0);

pVertices[vertexcount+0].uv=float2(0.01f,0.99f);

pVertices[vertexcount+1].position=float3(-1.0f,1.0f,0.0f);

pVertices[vertexcount+1].index= int3 (i, j,1);

pVertices[vertexcount+1].uv=float2(0.01f,0.01f);

pVertices[vertexcount+2].position=float3(1.0f,-1.0f,0.0f);

pVertices[vertexcount+2].index= int3 (i,j,2);

pVertices[vertexcount+2].uv=float2(0.99f,0.99f);

pVertices[vertexcount+3].position=float3(1.0f,1.0f,0.0f);

pVertices[vertexcount+3].index= int3 (i,j,3);

pVertices[vertexcount+3].uv=float2(0.99f,0.01f);

vertexcount+=4;

}

}

ret=g_pVB->Unlock();

//設定索引

int* pIndices=NULL;

ret=g_pIB->Lock(0,0,(void**)&pIndices,0);

vertexcount=0;

for(int i=0;i

{

for(int j=0;j

{

pIndices[0]=vertexcount+0;

pIndices[1]=vertexcount+1;

pIndices[2]=vertexcount+2;

pIndices[3]=vertexcount+2;

pIndices[4]=vertexcount+1;

pIndices[5]=vertexcount+3;

vertexcount+=4;

pIndices+=6;

}

}

ret=g_pIB->Unlock();

1.4 渲染時粒子系統參數的傳遞

接下來就是在渲染函數ParticleSystemManager::Render()把多個粒子系統的參數一次送到GPU的常量寄存器。首先計算總共有多少個批次batchnumber,用總共要繪制的特效系統執行個體數除以MAX_PARTICLE_SYSTEM_BATCH等到。接下來循環對每個批次中的各個粒子系統,計算該批次中粒子系統數量batchsize,等到每個粒子系統執行個體的資料指針pEntity,得到執行個體中用到的特效中繼資料指針pEffectElement和粒子系統資料指針pParticleSystem,接下來把這些參數編排到一個float4數組構成的結構ParticleSystemParameters,把這個資料通過

g_pEffect->SetFloatArray("batchdata",(float*)(&ParticleSystemParameters[0]),28*batchsize)一次設定到常量寄存器,通過ret=g_pEffect->SetFloat("batchstart",(float)batchstart)設定這批次的起始粒子系統序号,g_pEffect->SetTexture("particletexture",GEffectManager::GetTexture())設定合并後的特效紋理

static GParticleSystemParameter ParticleSystemParameters[MAX_PARTICLE_SYSTEM_BATCH];

UINT cPasses2=0;

ret=g_pEffect->Begin(&cPasses2,D3DXFX_DONOTSAVESTATE);

int batchnumber=nEntityNum/MAX_PARTICLE_SYSTEM_BATCH+1;

for(int batchindex=0;batchindex

{

int batchsize=MAX_PARTICLE_SYSTEM_BATCH;

if (batchindex==batchnumber-1)

batchsize=nEntityNum%MAX_PARTICLE_SYSTEM_BATCH;

int batchstart=batchindex*MAX_PARTICLE_SYSTEM_BATCH;

for (int i=0;i

{

GEffectElementEntity* pEntity=&pElementEntities[batchstart+i];

GEffectElement* pEffectElement=&pEntity->EffectElement;

GParticleSystem* pParticleSystem=(GParticleSystem*)(&pEffectElement->data[0]);

ParticleSystemParameters[i].parameters[0]=float4(vTranslation.x,vTranslation.y,vTranslation.z,pParticleSystem->m_fEmissionInterval);

ParticleSystemParameters[i].parameters[1]=float4(pParticleSystem->m_vMinVelocity.x,pParticleSystem->m_vMinVelocity.y,pParticleSystem->m_vMinVelocity.z,pParticleSystem->m_fParticlesPerEmission);

ParticleSystemParameters[i].parameters[2]=float4(pParticleSystem->m_vMaxVelocity.x,pParticleSystem->m_vMaxVelocity.y,pParticleSystem->m_vMaxVelocity.z,pParticleSystem->m_fMinLifeSpan);

ParticleSystemParameters[i].parameters[3]=float4(pParticleSystem->m_vAcceleration.x,pParticleSystem->m_vAcceleration.y,pParticleSystem->m_vAcceleration.z,pParticleSystem->m_fMaxLifeSpan);

ParticleSystemParameters[i].parameters[4]=float4(fSize,pParticleSystem->m_fWidthRatio,pParticleSystem->m_fMinAngularVelocity,pParticleSystem->m_fMaxAngularVelocity);

ParticleSystemParameters[i].parameters[5]=float4(pParticleSystem->m_fMinRadius,pParticleSystem->m_fMaxRadius,pParticleSystem->m_eBlendModel,(float)pEffectElement->nTextureIndex);

ParticleSystemParameters[i].parameters[6]=vColor;

}

ret=g_pEffect->SetFloat("batchstart",(float)batchstart);

ret=g_pEffect->SetFloatArray("batchdata",(float*)(&ParticleSystemParameters[0]),28*batchsize);

ret=g_pEffect->SetTexture("particletexture",GEffectManager::GetTexture());

for (int iPass = 0; iPass < (int)cPasses2; iPass++)

{

ret=g_pEffect->BeginPass(iPass);

ret=pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0, 0 ,batchsize*MAX_PARTICLE_NUMBER*4, 0 ,batchsize*MAX_PARTICLE_NUMBER*2);

ret=g_pEffect->EndPass();

}

}

ret=g_pEffect->End();

1.5粒子系統批量繪制的Shaders

接下來,我們開始編寫FX檔案來實作批量繪制。在Vertex Shader中,首先從BLENDINDICES語義中讀入每個頂點的三個索引: i粒子系統在這批次中的序号,j粒子在單個粒子系統中的序号,k頂點在粒子面片中的序号。接下來根據序号從常量參數batchdata讀出單個粒子系統的所有參數parameter0~parameter7。接下來一個很重要的事情就是為這些粒子根據序号j+i+batchstart作為種子編寫的線性同餘法産生确定性的僞随機數,用于後面給每個粒子在系統限制的最小最大速度,最小最大面積等之間配置設定數值,由此得到4個僞随機數randnum0~randnum4,并把前3個僞随機數寫成3維向量randthree友善後面運算。接下來從parameter0~parameter7提取需要的參數。然後根據這些參數構造一系列公式計算粒子的具體目前參數,這些公式是本文的主要貢獻之一。

粒子周期内時間:

Half ftime=saturate ( fmod ( time – floor ( j/ fParticlesPerEmission ) *fEmissionInterval , fTotalEmissionInterval ) / fLifeSpan ) * fLifeSpan;

上面這條公式首先對目前絕對時間time,根據初始相位進行偏移。由于每次發射fParticlesPerEmission 個粒子,那麼j/ fParticlesPerEmission 就是第j個粒子所處的發生起始次數,乘以粒子發射間隔fEmissionInterval 就得到該粒子的初始相位floor ( j/ fParticlesPerEmission ) *fEmissionInterval。把這個相位疊加到絕對時間,對該粒子總的發射間隔fTotalEmissionInterval求模就得到該第j個粒子的發生區間。這裡fEmissionInterval是從起始時間算起單個粒子系統中每批粒子發射間隔,fTotalEmissionInterval則是第j個粒子兩次發射之間的間隔,因為粒子在GPU中周期到了會循環再利用。

粒子的循環再用周期:

half fTotalEmissionInterval = max ( MAX_PARTICLE_NUMBER * fEmissionInterval / fParticlesPerEmission , fLifeSpan );

fTotalEmissionInterval是第j個粒子兩次發射之間的間隔,當該粒子的生命周期大于同一個系統中所有粒子循環再用的間隔,則以該粒子的生命周期作為循環再用周期,否則以同一個系統中所有粒子循環再用的間隔作為該粒子循環再用的周期。

粒子的生命周期:

half fLifeSpan=lerp(fMinLifeSpan,fMaxLifeSpan,randnum3);

粒子的生命周期根據前面算出的僞随機數在該系統的最小生命周期和最大生命周期作線性插值得到,由于僞随機數randnum3是根據粒子系統的序号和系統中的粒子序号j+i+batchstart作種子得到的,是以每個粒子得到的這個随機數都不一樣,但又是确定的,也就是說隻要粒子系統中的粒子序号j,粒子系統序号i,這批次中粒子系統起始序号batchstart都确定,這個随機數也就确定,生命周期也就随之确定。

粒子的速度:

half3 velocity=lerp(minvelocity,maxvelocity,randthree);

類似地,根據三維僞随機向量randthree在粒子系統的最小最大速度之間線性插值出該粒子的速度,這個得到的速度向量velocity也是關于j,i,batchstart具有确定性的。

粒子的角速度:

half fAngularVelocity=lerp(fMinAngularVelocity,fMaxAngularVelocity,randnum3);

half fAngular=fAngularVelocity*time;

half2 rotatevec=half2(cos(fAngular),sin(fAngular));

initpos.xy=half2(initpos.x*rotatevec.x-initpos.y*rotatevec.y,initpos.y*rotatevec.x+initpos.x*rotatevec.y);

類似地,根據僞随機數randnum3在粒子系統的最小最大角速度之間線性插值出該粒子的角速度,這個得到的角速度fAngularVelocity也是關于j,i,batchstart具有确定性的。目前的旋轉角度fAngular就是角速度乘以時間fAngularVelocity*time。根據這個角度fAngular可以構造一個選擇複數向量rotatevec,根據複數乘法對原頂點坐标向量initpos乘以旋轉複數向量rotatevec得到旋轉後的頂點坐标向量。

粒子的位移:

half3 localpos=velocity*ftime+0.5f*acceleration*ftime*ftime;

中學實體公式,就不解析了。

粒子的面積:

half fMinSize=parameter4.x;

half fMaxSize=parameter4.y;

half fSize=lerp(fMinSize,fMaxSize,randnum3);

half fWidthRatio=parameter4.y;

half3 objectpos= initpos*half3(fSize,fSize*fWidthRatio,fSize);

half3 billboardpos =mul((half3x3)View, objectpos);

類似地,根據僞随機數randnum3在粒子系統的最小最大面積之間線性插值出該粒子的面積,這個得到的面積fSize同樣具有關于j,i,batchstart的确定性。把面積勝于長寬比構造出目前頂點在對象空間的位置向量

initpos*half3(fSize,fSize*fWidthRatio,fSize)。把這個向量作為右矩陣跟目前的視錐矩陣View的3X3部分作乘法得到沿鏡頭朝着使用者眼睛的頂點坐标。這裡值得注意的是mul第一個參數是矩陣的3X3部分因為這裡是要根據View矩陣的三個向量構造一個微分幾何的Frenet标架,用于變換對象空間的頂點向量在世界空間朝着錄影機。這個乘法等價于:

half3 xCamera=normalize(half3(View[0][0],View[1][0],View[2][0]));

half3 yCamera=normalize(half3(View[0][1],View[1][1],View[2][1]));

half3 zCamera=normalize(half3(View[0][2],View[1][2],View[2][2]));

half3 billboardpos = objectpos.x*xCamera+ objectpos.y*yCamera+ objectpos.z*zCamera;

粒子的發生半徑:

half fMinRadius=parameter5.x;

half fMaxRadius=parameter5.y;

half fRadius=lerp(fMinRadius,fMaxRadius,randnum3);

half3 offsetpos=(randthree*2.0f-1.0f)*fRadius;

類似地,根據僞随機數randnum3在粒子系統的最小最大發射半徑之間線性插值出該粒子的發射半徑,這個得到的發射半徑fRadius也是關于j,i,batchstart具有确定性的。

頂點的世界空間坐标:

half4 worldpos=half4(billboardpos+localpos+offsetpos+translation,1.0f);

頂點的世界坐标就是把前面算好的特效執行個體的平移值,該粒子的發射半徑,粒子的位移,頂點在對象空間的位置加起來得到。

頂點的紋理坐标:

half textureid=parameter5.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

頂點的紋理坐标就是把原始的紋理坐标變換到合并後的特效貼圖上的紋理坐标。先讀取目前特效用到的紋理序号textureid,求出這個紋理在合并後的紋理的位置(globalu,gloablv)把這個位置加上原始頂點在自己紋理中的采樣位置就得到新的變換後的紋理坐标。

特效的混合模式:

特效的混合模式是指特效與幀緩沖的alpha混合模式,在編輯器裡有幾種設定: 0暗混合(srcblend= srcalpha, destblend=invsrcalpha),1亮混合(srcblend=srcalpha,destblend=one)。如果混合模式不能統一,那麼不同的混合模式就得分組繪制,會産生更多的批次。是以我們想方設法進行合并。首先看看這兩種混合的公式:

暗混合:Cf= AsCs+AdCd; Ad=1-As

亮混合:Cf=AsCs+AdCd; Ad=1

Cf是幀緩沖混合完的顔色,Cs是Pixel Shader輸出的顔色,As是Pixel Shader輸出的alpha值,Cd是幀緩沖混合前的顔色。是以要對這混合模式進行合并就是得同時逐特效指定As和Ad。但是目前的D3D隻支援指定這兩者的其中一個。由于幀緩沖混合前的顔色Cd是無法修改的,如果我們在Pixel Shader中指定As,我們必須修改destblend這個狀态。盡管我們無法修改幀緩沖的顔色,但是我們修改Pixel Shader輸出的顔色,也可以用Pixel Shader輸出的alpha值As’作為混合中幀緩沖的Alpha值(destblend= srcalpha)同時讓Pixel Shader輸出的顔色直接乘上原來需要的源混合因子As,即srcblend=1且Cs’=AsCs。

暗混合:Cf= Cs’+As’Cd; As’=1-As, Cs’=AsCs

亮混合:Cf=Cs’+As’Cd; As’=1, Cs’=AsCs

對上面兩個式子合并一下有:

Cf=Cs’+As’Cd; As’=1-(1-blend)As

當blend=0暗混合模式時, As’=1-As; 當blend=1亮混合模式時, As’=1

最終我們有下面這段詭異的Shader

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

srcblend=one;

destblend=srcalpha;

下面是FX檔案中主要的Shaders:

uniform float4 batchdata[238];

float rand(float value)

{

float n=511123;

float b=534;

return fmod(value*n+b,RANDMAX+1.0f);

}

Vertex VS_main(half3 initpos: POSITION0,half3 localindex: BLENDINDICES, half2 uv0: TEXCOORD0)

{

Vertex Out;

int i=localindex.x; //particle system index

int j=localindex.y; //particle index

int k=localindex.z; //vertex index

half4 parameter0=batchdata[i*7+0];

half4 parameter1=batchdata[i*7+1];

half4 parameter2=batchdata[i*7+2];

half4 parameter3=batchdata[i*7+3];

half4 parameter4=batchdata[i*7+4];

half4 parameter5=batchdata[i*7+5];

half4 parameter6=batchdata[i*7+6];

half randnum0=rand(j+i+batchstart)/RANDMAX;

half randnum1=rand(randnum0)/RANDMAX;

half randnum2=rand(randnum1)/RANDMAX;

half randnum3=rand(randnum2)/RANDMAX;

half3 randthree=half3(randnum0,randnum1,randnum2);

half fEmissionInterval=parameter0.w;

half fParticlesPerEmission=parameter1.w;

half fMinLifeSpan=parameter2.w;

half fMaxLifeSpan=parameter3.w;

half fLifeSpan=lerp(fMinLifeSpan,fMaxLifeSpan,randnum3);

half fTotalEmissionInterval=max(MAX_PARTICLE_NUMBER * fEmissionInterval/fParticlesPerEmission,fLifeSpan);

half ftime=saturate(fmod(time-floor(j/fParticlesPerEmission)*fEmissionInterval,fTotalEmissionInterval)/fLifeSpan)*fLifeSpan;

half3 translation=parameter0.xyz;

half3 minvelocity=parameter1.xyz;

half3 maxvelocity=parameter2.xyz;

half3 acceleration=parameter3.xyz;

half3 velocity=lerp(minvelocity,maxvelocity,randthree);

half3 localpos=velocity*ftime+0.5f*acceleration*ftime*ftime;

half fMinAngularVelocity=parameter4.z;

half fMaxAngularVelocity=parameter4.w;

half fAngularVelocity=lerp(fMinAngularVelocity,fMaxAngularVelocity,randnum3);

if (fAngularVelocity>0.01f)

{

half fAngular=fAngularVelocity*time;

half2 rotatevec=half2(cos(fAngular),sin(fAngular));

initpos.xy=half2(initpos.x*rotatevec.x-initpos.y*rotatevec.y,initpos.y*rotatevec.x+initpos.x*rotatevec.y);

}

half fMinSize=parameter4.x;

half fMaxSize=parameter4.y;

half fSize=lerp(fMinSize,fMaxSize,randnum3);

half fWidthRatio=parameter4.y;

half3 objectpos= initpos*half3(fSize,fSize*fWidthRatio,fSize);

half3 billboardpos=mul((half3x3)View,objectpos);

half fMinRadius=parameter5.x;

half fMaxRadius=parameter5.y;

half fRadius=lerp(fMinRadius,fMaxRadius,randnum3);

half3 offsetpos=(randthree*2.0f-1.0f)*fRadius;

half4 worldpos;

worldpos.xyz=billboardpos+localpos+offsetpos+translation;

worldpos.w=1.0f;

Out.worldpos=worldpos;

half4 prjpos=mul(worldpos, ViewProjection); //把頂點從世界空間變換到投影空間

Out.Pos = prjpos;

half textureid=parameter5.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

Out.color=parameter6;

Out.color.a*=1.0f-floor(ftime/fLifeSpan);

Out.blend=parameter5.z;

return Out;

}

half4 PS_main(half4 worldpos:TEXCOORD0,half2 uv:TEXCOORD1,half4 color:TEXCOORD2,half blend:TEXCOORD3) : COLOR0

{

half4 result=tex2D(ParticleSampler,uv)*color;

//遠處雲霧淡出

half fdistance=distance(worldpos.xz,CameraEye.xz);

half ffog=(fdistance-ffogstart)/(ffogend-ffogstart);

result.a*=1.0f-ffog;

//原紋理的混合比例直接讓顔色乘以源的比例

//目的的比例通過destblend=srcalpha來設上去,因為混合模式不一樣這個值不一樣,用這個來根據混合模式設定幀緩沖的alpha混合比例

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

}

technique Render

{

pass Single_Pass

{

AlphaBlendEnable =true;

srcblend=one; //值得注意

destblend=srcalpha; //值得注意

Lighting = false;

cullmode=none;

zenable=true;

zwriteenable=false;

fogenable=false;

VertexShader = compile vs_2_0 VS_main();

PixelShader=compile ps_2_0 PS_main();

}

}

2. 公告闆的批量繪制

相對來說,公告闆特效的合并要簡單些,公告闆可以看作是粒子系統中隻有一個粒子的粒子系統。

2.1頂點緩沖和索引緩沖的建立填充

首先,我們需要建立足夠存儲多個粒子系統的所有粒子的頂點緩沖和索引緩沖:

ret=pd3dDevice->CreateVertexBuffer(MAX_SPLITE_BATCH*4*sizeof(PNCT1Vertex),0, D3DFVF_PNCT1Vertex,D3DPOOL_MANAGED, &g_pVB, NULL );

ret=pd3dDevice->CreateIndexBuffer(MAX_SPLITE_BATCH*6*sizeof(int),0,D3DFMT_INDEX32,D3DPOOL_MANAGED,&g_pIB,NULL);

MAX_SPLITE_BATCH是公告闆的數量

所有批次的公告闆都是共用這個頂點緩沖和索引緩沖,接下來就是填充頂點和索引資料,這裡每個頂點的position存的是構成每個粒子的四邊形面片的每個頂點在對象空間中的坐标,Shader的語義是POSITION0;每個頂點的索引index的三個分量存的是i公告闆的序号,固定的一個0值(公布闆内沒有再細分的元素了)和頂點在公告闆面片4個頂點中的序号,語義是BLENDINDICES;每個頂點的uv存放的是紋理坐标,這裡減少了0.01f是為了在合并後的紋理采樣時看不到紋理間的裂縫。

vertexcount=0;

for(int i=0;i

{

pVertices[vertexcount+0].position=float3(-1.0f,-1.0f,0.0f);

pVertices[vertexcount+0].index=int3(i,0,0);

pVertices[vertexcount+0].uv=float2(0.01f,0.99f);

pVertices[vertexcount+1].position=float3(-1.0f,1.0f,0.0f);

pVertices[vertexcount+1].index = int3 (i,0,1);

pVertices[vertexcount+1].uv=float2(0.01f,0.01f);

pVertices[vertexcount+2].position=float3(1.0f,-1.0f,0.0f);

pVertices[vertexcount+2].index = int3 (i,0,2);

pVertices[vertexcount+2].uv=float2(0.99f,0.99f);

pVertices[vertexcount+3].position=float3(1.0f,1.0f,0.0f);

pVertices[vertexcount+3].index = int3 (i,0,3);

pVertices[vertexcount+3].uv=float2(0.99f,0.01f);

vertexcount+=4;

}

2.2公告闆批量繪制的Shaders

公告闆和粒子系統的主要差别是,公告闆的旋轉要根據軸來進行,要根據公告闆的角速度fAngularVelocity計算出旋轉角度fAngular,進而根據旋轉軸rotatedir構造一個旋轉矩陣matRotate進行旋轉。這裡的角速度fAngularVelocity就是輸入的旋轉向量rotatevec的模長,旋轉軸rotatedir就是旋轉向量rotatevec的機關向量。輸入的zPlane是公告闆的法線。對zPlane進行兩次叉乘,可以構造出一個以輸入法線zPlane為z軸,叉乘得到的xPlane和yPlane為x軸和y軸的3×3 Frenet标架(xPlane,yPlane,zPlane),也就是一個坐标基。是以根據輸入的法線zPlane可以計算出一個局部坐标objectpos0,根據錄影機的朝向View可以構造出另外一個朝着使用者的局部坐标objectpos1。對這兩個值作線性插值,當normaltype=0,全局法線模式時,objectpos取objectpos0,當normaltype=1,朝着鏡頭模式時,objectpos取objectpos1,也可以取浮點的normaltype在兩者之間取值。

half3 yPlane=cross(zPlane,half3(1,0,0));

half3 xPlane=cross(yPlane,zPlane);

half3 objectpos0=localpos.x*xPlane+localpos.y*yPlane+localpos.z*zPlane;

half3 objectpos1=mul((half3x3)View,localpos);

half3 objectpos=lerp(objectpos0,objectpos1,normaltype);

Vertex VS_main(half3 initpos: POSITION0,float3 localindex:NORMAL0, half2 uv0: TEXCOORD0)

{

Vertex Out;

int i=localindex.x; //splite system index [0~40]

int k=localindex.z; //vertex index

float4 parameter0=batchdata[i*6+0];

float4 parameter1=batchdata[i*6+1];

float4 parameter2=batchdata[i*6+2];

float4 parameter3=batchdata[i*6+3];

float4 parameter4=batchdata[i*6+4];

float4 parameter5=batchdata[i*6+5];

float randnum0=rand(i+batchstart)/1241.0f;

float randnum1=rand(randnum0)/1241.0f;

float randnum2=rand(randnum1)/1241.0f;

float randnum3=rand(randnum2)/1241.0f;

float fLifeSpan=parameter4.x;

float fLifeStart=parameter4.y;

float fTimeRange=parameter4.z;

float ftime=saturate((fmod(time-fLifeStart,fTimeRange))/fLifeSpan)*fLifeSpan;

half3 translation=parameter0.xyz;

half normaltype=parameter1.w;

half3 zPlane=normalize(parameter1.xyz);

half3 rotatevec=parameter2.xyz;

half fAngularVelocity=length(rotatevec);

half3 rotatedir=normalize(rotatevec);

half fSize=parameter3.x;

half fWidthRatio=parameter3.y;

half3 localpos=initpos*half3(fSize,fSize*fWidthRatio,fSize);

if (fAngularVelocity>0.01f)

{

half fAngular=fAngularVelocity*time;

float4x4 matRotate;

float fCos = cos( fAngular );

float fSin = sin( fAngular );

matRotate[0][0] = ( rotatedir.x * rotatedir.x ) * ( 1.0f - fCos ) + fCos;

matRotate[0][1] = ( rotatedir.x * rotatedir.y ) * ( 1.0f - fCos ) - (rotatedir.z * fSin);

matRotate[0][2] = ( rotatedir.x * rotatedir.z ) * ( 1.0f - fCos ) + (rotatedir.y * fSin);

matRotate[1][0] = ( rotatedir.y * rotatedir.x ) * ( 1.0f - fCos ) + (rotatedir.z * fSin);

matRotate[1][1] = ( rotatedir.y * rotatedir.y ) * ( 1.0f - fCos ) + fCos ;

matRotate[1][2] = ( rotatedir.y * rotatedir.z ) * ( 1.0f - fCos ) - (rotatedir.x * fSin);

matRotate[2][0] = ( rotatedir.z * rotatedir.x ) * ( 1.0f - fCos ) - (rotatedir.y * fSin);

matRotate[2][1] = ( rotatedir.z * rotatedir.y ) * ( 1.0f - fCos ) + (rotatedir.x * fSin);

matRotate[2][2] = ( rotatedir.z * rotatedir.z ) * ( 1.0f - fCos ) + fCos;

matRotate[0][3] = matRotate[1][3] = matRotate[2][3] = matRotate[3][0] = matRotate[3][1] = matRotate[3][2] = 0.0f;

matRotate[3][3] = 1.0f;

localpos=mul(localpos,matRotate);

}

half3 yPlane=cross(zPlane,half3(1,0,0));

half3 xPlane=cross(yPlane,zPlane);

half3 objectpos0=localpos.x*xPlane+localpos.y*yPlane+localpos.z*zPlane;

half3 objectpos1=mul((half3x3)View,localpos);

half3 objectpos=lerp(objectpos0,objectpos1,normaltype);

half4 worldpos;

worldpos.xyz=objectpos+translation;

worldpos.w=1.0f;

Out.worldpos=worldpos;

//把頂點從世界空間變換到投影空間

half4 prjpos=mul(worldpos, ViewProjection);

Out.Pos = prjpos;

half textureid=parameter0.w;

half globalu=fmod(textureid,16.0f)/16.0f;

half globalv=floor(textureid/16.0f)/16.0f;

Out.uv=half2(globalu,globalv)+uv0/16.0f;

Out.color=parameter5;

Out.color.a*=1.0f-floor(ftime/fLifeSpan);

Out.blend=parameter2.w;

return Out;

}

half4 PS_main(half4 worldpos:TEXCOORD0,half2 uv:TEXCOORD1,half4 color:TEXCOORD2,half blend:TEXCOORD3) : COLOR0

{

half4 result=tex2D(SpliteSampler,uv)*color;

//遠處雲霧淡出

half fdistance=distance(worldpos.xz,CameraEye.xz);

half ffog=(fdistance-ffogstart)/(ffogend-ffogstart);

result.a*=1.0f-ffog;

//原紋理的混合比例直接讓顔色乘以源的比例

//目的的比例通過destblend=srcalpha來設上去,因為混合模式不一樣這個值不一樣,用這個來根據混合模式設定幀緩沖的alpha混合比例

result.rgb*=result.a;

result.a=1.0f-(1.0f-blend)*result.a;

return result;

}

3. 特效紋理的合并

這個就比較簡單了,就是把紋理進行一下拼圖算法。這個紋理是各種特效粒子系統,公告闆,圖元軌迹,模型特效等共用的。下面是一個合并的特效紋理例子。

特效的批量繪制

繼續閱讀