这篇文章的主要内容是Dx10的一个例子,ParticlesGS Sample,Siney最近在研究dx10,说起来,已经“研究”好久了,但一直没有系统的写点例子,了解整个dx10的架构,所以写一篇dx10的文章,1)是总结学习,2)是整理下思路,根据自己的理解把相关的内容讲述清楚。

dx10与dx9相比,有很多变化,其中之一就是渲染管线的变化,如图:

image

可以看到,主要是加入了Stream Output和Geometry-Shader阶段,而ParticlesGS Sample正好演示这两个阶段的用处和功能,这个例子演示了如何实现一个基于gs的粒子系统,该粒子系统是模拟焰火,由于焰火需要爆炸出新例子的特性,使用gs可以动态构建新primitive的特性,可以很方便的模拟这个系统,在初始阶段,我只要在IA阶段传入一个luancher粒子,他的作用就是定时的产生新的shell例子,然后shell粒子经过一段时间后,爆炸出n多ember粒子,ember粒子再经过一段时间后就消亡了,往复这个过程,我们就可以看见一个焰火的模拟例子系统。

以luancher产生shell粒子为例,可以看到gs如下:

[maxvertexcount(128)]

void GSAdvanceParticlesMain(point VSParticleIn input[1], inout PointStream<VSParticleIn> ParticleOutputStream)
{
    if( input[0].Type == PT_LAUNCHER )
        GSLauncherHandler( input[0], ParticleOutputStream );
    else if ( input[0].Type == PT_SHELL )
        GSShellHandler( input[0], ParticleOutputStream );
    else if ( input[0].Type == PT_EMBER1 ||
              input[0].Type == PT_EMBER3 )
        GSEmber1Handler( input[0], ParticleOutputStream );
    else if( input[0].Type == PT_EMBER2 )
        GSEmber2Handler( input[0], ParticleOutputStream );
}

void GSLauncherHandler( VSParticleIn input, inout PointStream<VSParticleIn> ParticleOutputStream )
{
    if(input.Timer <= 0)
    {
        float3 vRandom = normalize( RandomDir( input.Type ) );
        //time to emit a new SHELL
        VSParticleIn output;
        output.pos = input.pos + input.vel*g_fElapsedTime;
        output.vel = input.vel + vRandom*8.0;
        output.Timer = P_SHELLLIFE + vRandom.y*0.5;
        output.Type = PT_SHELL;
        ParticleOutputStream.Append( output );
        //reset our timer
        input.Timer = g_fSecondsPerFirework + vRandom.x*0.4;
    }
    else
    {
        input.Timer -= g_fElapsedTime;
    }
    //emit ourselves to keep us alive
    ParticleOutputStream.Append( input );
}

除去vs和ps没有的语法外,上述gs还是蛮好理解的,简单来说,就是在定时器结束的时候,new一个新的shell例子,并Append到ParticleOutputStream,注意这里的primitive为point,而不是triangle,所以在后面渲染的时候,我们还需要在gs中,展开为triangle以渲染一个billboard。还需要注意的是,我们需要重置luancher例子的计时器,并也把他Append到ParticleOutputStream,没有了这个发动机,下次就不会产生shell例子了。

上述gs代码需要stream output,如下:

// Point to the correct output buffer
pBuffers[0] = g_pParticleStreamTo;
pd3dDevice->SOSetTargets( 1, pBuffers, offset );

[maxvertexcount(128)]的语意是告诉gpu,这段gs代码最多会产生128个顶点,当然也可能没有这么多,比如产生shell例子就仅需要[maxvertexcount(1)],而128这个数值是针对shell产生ember粒子。能够动态产生point的粒子了,我们还需要渲染这些粒子,我们都知道粒子的渲染一般是billboard,而仅仅是一个point是不能构成billboard,所以我们还需要一段gs,展开point成为2个triangle,如下gs:

[maxvertexcount(4)]
void GSScenemain(point VSParticleDrawOut input[1], inout TriangleStream<PSSceneIn> SpriteStream)
{
    PSSceneIn output;
    //
    // Emit two new triangles
    //
    for(int i=0; i<4; i++)
    {
        float3 position = g_positions[i]*input[0].radius;
        position = mul( position, (float3x3)g_mInvView ) + input[0].pos;
        output.pos = mul( float4(position,1.0), g_mWorldViewProj );
        output.color = input[0].color;
        output.tex = g_texcoords[i];
        SpriteStream.Append(output);
    }
    SpriteStream.RestartStrip();
}

这段gs,产生4个vertex,其实就是strip的4边形,最后需要RestartStrip(),告诉gpu一段strip完成,这样就会产生独立的4边形,否则可能会导致后续的vertex也进入strip,从而渲染错误。

在开始渲染之前,我们需要关闭stream output,否则后续的gs代码又会stream output,如下:

// Get back to normal
pBuffers[0] = NULL;
pd3dDevice->SOSetTargets( 1, pBuffers, offset );

这样后续的gs阶段后,会进入ps阶段。

最后我们还需要注意,由于在gs中我们可能增加vertex,也可能不增加,但对于外部的draw函数来说,并不知道实际的顶点数量,或者primitive数量,这使得我们不能直接调用传统的Draw函数,为此dx10为我们提供了DrawAuto函数,他会根据gpu实际产生的primitive数量来渲染,不需要任何参数。

用gs实现粒子系统确实很方便,也很高效,与传统的基于dx9的粒子系统,我们避免了频繁的lock & unlock,形体计算等cpu密集的操作,而是完全交给gpu去计算、更新,能够更好的并行cpu和gpu,最大发挥gpu的威力。