特效的批量绘制
特效的批量绘制
张嘉华(newzjh@126.com)
特效系统是游戏中的一个重要组成部分,在场景布谷,角色技能等有广泛应用。一个特效往往包含多种多样的组成元素:粒子系统,公告板,音效,图元轨迹,模型特效,镜头滤镜/震动/模糊,动态光源等。其中以粒子系统和公告板在场景中用得最多,往往一个特效由上述这些元素构成多个轨道,沿着时间轴以不同起始时间(相位)和周期播放。批量绘制和几何实例化(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<MAX_PARTICLE_SYSTEM_BATCH;i++)
{
for(int j=0;j<MAX_PARTICLE_NUMBER;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<MAX_PARTICLE_SYSTEM_BATCH;i++)
{
for(int j=0;j<MAX_PARTICLE_NUMBER;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<batchnumber;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<batchsize;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<MAX_SPLITE_BATCH;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. 特效纹理的合并
这个就比较简单了,就是把纹理进行一下拼图算法。这个纹理是各种特效粒子系统,公告板,图元轨迹,模型特效等共用的。