• 处理2D图像和纹理——创建一个3D爆炸效果,简单的粒子系统


    问题

    你想在3D世界中创建一个漂亮的3D爆炸效果。

    解决方案

    你可以通过混合大量的小火焰图像(叫做粒子)创建一个爆炸效果,,如图3-22所示。注意一个单独的粒子很暗,但是,如果你添加大量的粒子,就会获得一个漂亮的爆炸效果。

    1

    图3-22 单个爆炸粒子

    在爆炸的开始阶段,所有粒子都位于爆炸的初始位置,因为所有图像的颜色都叠加在了一起,所以会形成一个明亮的火球,绘制粒子时你需要使用additive blending才能获得这个效果。

    随着时间流逝,粒子会从初始位置离开。而且,在离开中心的过程中还要使每个粒子图像变得更小更暗(淡出)。

    一个漂亮的3D爆炸需要用到50至100个粒子。因为这些图像只是显示在3D世界中的2D图像,需要用到billboard,所以这个教程是建立在教程3-11基础上的。

    你将创建一个基于shader的实时粒子系统。每一帧中,计算每个粒子的存活时间(age),它的位置、颜色和大小是基于存活时间计算的。当然,应该在GPU中进行这些计算,让CPU可以完成更重要的工作。

    工作原理

    如前所述,本章会重用教程3-11的代码,所以需要理解球形billboarding的原理。对每个粒子,你仍需定义六个顶点,而且只需定义一次,将它们传递到显卡。对每个顶点,需要包含下列数据用于vertex shader:

    • 爆炸的初始位置的billboard中心点3D位置(用于billboarding) (Vector3 = 三个浮点数)
    • 纹理坐标(用于billboarding) (Vector2 = 两个floats)
    • 粒子创建的时间(一个浮点数)
    • 粒子的存活时间(一个浮点数)
    • 粒子移动的方向(Vector3 = 三个浮点数)
    • 一个随机数,用于给每个粒子施加不同的行为(一个浮点数)

    从这些数据,GPU可以基于当前时间计算所需的所有东西。例如,它可以计算粒子已经存活了多少时间。当你将这个存活时间乘以粒子的方向时,就可以获取粒子离开爆炸初始位置的距离。

    你需要一个顶点格式存储这个数据,使用一个Vector3存储位置;一个Vector4存储第二,第三,第四个数据;另一个Vector4存储最后两个数据。创建自定义顶点格式可参见教程5-14:

    public struct VertexExplosion 
    {
        public Vector3 Position; 
        public Vector4 TexCoord; 
        public Vector4 AdditionalInfo; 
        
        public VertexExplosion(Vector3 Position, Vector4 TexCoord, Vector4 AdditionalInfo) 
        {
            this.Position = Position; 
            this.TexCoord = TexCoord; 
            this.AdditionalInfo = AdditionalInfo; 
        }
        
        public static readonly VertexElement[] VertexElements = new VertexElement[] 
        {
            new VertexElement(0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0), 
            new VertexElement(0, 12, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 0), 
            new VertexElement(0, 28, VertexElementFormat.Vector4, VertexElementMethod.Default, VertexElementUsage.TextureCoordinate, 1), 
        }; 
        
        public static readonly int SizeInBytes = sizeof(float) * (3 + 4 + 4); 
    } 

    这看起来与教程3-11中的很像,除了用一个Vector4代替Vector2作为第二个参数,让额外的两个浮点数可以传递到vertex shader。要创建随机方向,需要使用一个randomizer,所以在XNA类中添加以下变量:

    Random rand;

    在Game类的Initialize方法中进行初始化:

    rand = new Random(); 

    现在可以创建顶点了。下面的方法基于教程3-11生成顶点:

    private void CreateExplosionVertices(float time) 
    {
        int particles = 80; 
        explosionVertices = new VertexExplosion[particles * 6]; 
        
        int i = 0; 
        for (int partnr = 0; partnr < particles; partnr++) 
        {
            Vector3 startingPos = new Vector3(5,0,0); 
            float r1 = (float)rand.NextDouble() - 0.5f; 
            float r2 = (float)rand.NextDouble() - 0.5f; 
            float r3 = (float)rand.NextDouble() - 0.5f;
            Vector3 moveDirection = new Vector3(r1, r2, r3); 
            moveDirection.Normalize(); 
            
            float r4 = (float)rand.NextDouble(); 
            r4 = r4 / 4.0f * 3.0f + 0.25f; 
            
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 1, time, 1000), new Vector4(moveDirection,r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 0, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 0, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 1, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 1, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 0, time, 1000), new Vector4(moveDirection, r4)); 
        }
    } 

    这个方法需要在初始化一个新爆炸时调用,它接受当前时间为参数。首先定义了爆炸中使用的粒子数量,创建了一个数组保存每个粒子的六个顶点。然后,创建这些顶点。对每个顶点,首先储存startingPos,即爆炸的中心点。

    然后,你想让每个粒子都有不同的方向。这可以通过生成两个[0,1]区间的随机数实现,但需要减去0.5使它们落在[–0.5f, +0.5f]区间。基于这个随机值创建一个Vector3,归一化这个Vector3让随机方向具有相同的长度。

    技巧:推荐使用归一化,因为向量(0.5f, 0.5f, 0.5f)比向量(0.5f, 0, 0)长。所以,当你增加第一个向量的位置时,如果使用第二个向量作为运动方向,粒子会运动得更快。结果是,爆炸会变得像个立方体而不是球形。

    然后,获取另一个随机值,用来给每个粒子施加各自的效果,这是因为粒子的速度和大小都要由这个值进行调整。要求有一个介于0和1之间的随机数,然后缩放到[0.25f, 1.0f]区间(谁想要一个速度为的粒子?)。

    最后,将每个粒子的六个顶点添加到顶点数组中。注意每六个顶点都包含相同的信息,除了纹理坐标,纹理坐标用来在vertex shader定义每个顶点偏离中心位置的偏移量。

    HLSL

    现在准备好了顶点,可以编写vertex和pixel shader了。

    首先定义XNA-HLSL变量,纹理采样器,vertex shader和pixel shader的输出结构:

    //XNA interface 
    float4x4 xView; 
    float4x4 xProjection; 
    float4x4 xWorld; 
    float3 xCamPos; 
    float3 xCamUp; 
    float xTime; 
    
    //Texture Samplers 
    Texture xExplosionTexture; 
    sampler textureSampler = sampler_state 
    {
        texture = <xExplosionTexture>;
        magfilter = LINEAR;
        minfilter = LINEAR; 
        mipfilter=LINEAR; 
        AddressU = CLAMP; 
        AddressV = CLAMP; 
    }; 
    
    struct ExpVertexToPixel 
    {
        float4 Position : POSITION; 
        float2 TexCoord : TEXCOORD0; 
        float4 Color : COLOR0; 
    }; 
    
    struct ExpPixelToFrame 
    {
        float4 Color : COLOR0; 
    };

    因为爆炸中的每个粒子都要施加billboard,你需要相同的变量。这次,你还需要在XNA程序的xTime变量中存储当前时间,让vertex shader可以计算每个粒子的生存时间。

    如教程3-11所述,vertex shader将2D屏幕位置和纹理坐标传递到pixel shader中。因为你想实现粒子的淡出效果,所以还要从vertex shader中传递颜色信息。和往常一样,pixel shader只计算每个像素的颜色。

    Vertex Shader

    vertex shader对顶点施加billboard,所以使用以下代码,这个代码来自于教程3-11中的球形billboarding:

    //Technique: Explosion
    float3 BillboardVertex(float3 billboardCenter, float2 cornerID, float size) 
    {
        float3 eyeVector = billboardCenter - xCamPos; 
        float3 sideVector = cross(eyeVector,xCamUp); 
        sideVector = normalize(sideVector); 
        float3 upVector = cross(sideVector,eyeVector); 
        upVector = normalize(upVector); 
        float3 finalPosition = billboardCenter; 
        finalPosition += (cornerID.x-0.5f)*sideVector*size; 
        finalPosition += (0.5f-cornerID.y)*upVector*size; 
        
        return finalPosition; 
    } 

    简而言之,你将billboard的中心位置,纹理坐标(用来区分当前顶点)和billboard大小传递到这个方法,这个方法返回顶点的3D位置。更多的信息可参见教程3-11。

    下面是vertex shader,它将使用刚才定义的方法:

    ExpVertexToPixel ExplosionVS(float3 inPos: POSITION0, float4 inTexCoord: TEXCOORD0, float4 inExtra: TEXCOORD1)
    {
        ExpVertexToPixel Output = (ExpVertexToPixel)0; 
        
        float3 startingPosition = mul(inPos, xWorld); 
        float2 texCoords = inTexCoord.xy; 
        float birthTime = inTexCoord.z; 
        float maxAge = inTexCoord.w; 
        float3 moveDirection = inExtra.xyz; 
        float random = inExtra.w; 
    }

    vertex shader接受存储在每个顶点中的一个Vector3和两个Vector4。首先,将数据中的内容存储在一些变量中。Billboard中心位置存储在Vector3中。你已经定义了第一个Vector4中的x和y分量包含纹理坐标,第三第四个分量包含粒子创建的时间和存活时间。第二个Vector4包含粒子移动的方向和一个额外的随机浮点数。

    现在,你可以将当前时间减去birthTime获取粒子已经存在的时间,存储在xTime变量中。但是,当使用time span时,你想用0和1之间的相对值表示这个时间,0表示开始时刻,1表示结束时刻。所以要将age除以maxAge, maxAge表示此时粒子应该“死亡”。

    float age = xTime - birthTime; float relAge = age/maxAge; 

    当粒子是新的时,xTime与birthTime相同,relAge 为0。当接近于结束,age几乎等于maxAge,relAge接近于1。

    首先根据粒子的age 调整它的大小。你想让每个粒子开始时最大然后慢慢变小。但是,你不想让它完全消失,所以需要使用以下代码:

    float size = 1-relAge*relAge/2.0f; 

    看起来有点难以理解,如果用图表示可以帮助你理解这个代码,如图3-22左图所示。对横轴上的每个粒子的Age,你可以在在纵轴上找到对应的大小。例如,relAge = 0时大小为1,relAge = 1时大小为0.5f。

    2

    图3-23 Size-relAge函数; displacement-relAge函数

    但是,当relAge为1时会继续减小。这意味着size会变为负值(如下面的代码所见,这会导致图像会在反方向扩展)。所以,你需要将这个值 处于0和1之间。

    因为大小为1的粒子太小,只需简单地将它们乘以一个数字进行缩放,这里是5。要让每个粒子各不相同,你还要将大小乘以一个随机数:

    float sizer = saturate(1-relAge*relAge/2.0f); 
    float size = 5.0f*random*sizer; 

    现在粒子的大小随着时间的流逝会变小,下一步是计算粒子中心的3D位置。你已经知道初始3D位置(爆炸的中心位置)和粒子的移动方向。你需要知道粒子已经沿着这个方向移动了多远,当然,这个距离对应粒子的生存时间。一个简单的方法是让粒子以一个不变的速度移动,但是实际情况中这个速度会减小。

    看一下图3-23中的右图,水平轴表示粒子的生存时间,竖直轴表示粒子离开中心位置的距离。你看到距离一开始是线性增加的,但过了一会儿,这个距离增加速度减小,最后不变。这意味着开始时速度保持不变最后为0。

    幸运的是,这个曲线只是简单的正弦函数的四分之一。对于一个任意给定的位于0和1之间的relAge,你可以使用这个函数找到对应的曲线上的距离:

    float totalDisplacement = sin(relAge*6.28f/4.0f); 

    一个完整的正弦曲线周期是从0到2*pi=6.28。因为你只想要四分之一周期,所以需要除以4。现在对于0和1之间的relAge,totalDisplacement拥有了一个对应图3-23右图所示的曲线的值。

    你还要将这个值乘以一个因子使粒子离开中心一点儿,本例中这个因子为3比较合适。更大的值导致更大的爆炸。这个值还要乘以一个随机值,使每个粒子都有自己的速度:

    float totalDisplacement = sin(relAge*6.28f/4.0f)*3.0f*random;

    一旦知道了粒子沿着运动方向运动的距离,就可以很简单地获取粒子的当前位置:

    float3 billboardCenter = startingPosition + totalDisplacement*moveDirection; 
    billboardCenter += age*float3(0,-1,0)/1000.0f;

    最后一行代码给粒子施加一个重力。因为xTime变量包含当前的以毫秒为单位的时间,这行代码会每秒让粒子下降一个单位。

    技巧:如果你想对一个移动的物体施加爆炸效果,例如一架飞机,你只需简单地添加一条代码让所有粒子移向物体移动的方向。你可以在顶点的一个额外的TEXCOORD2向量中传递这个方向。

    现在有了billboard 中心的3D位置和大小,你就做好了将这些值传递到billboarding方法中的准备:

    float3 finalPosition = BillboardVertex(billboardCenter, texCoords, size); 
    float4 finalPosition4 = float4(finalPosition, 1); 
    float4x4 preViewProjection = mul (xView, xProjection); 
    Output.Position = mul(finalPosition4, preViewProjection);

    这个代码来自于教程3-11。首先计算billboarded位置。和往常一样,这个3D位置需要乘以一个 4 × 4矩阵转换为2D屏幕位置,但在这之前,float3需要被转换到float4。你将最终的2D位置存储在Output结构的Position变量中。

    以上代码已经可以给出一个漂亮的结果,但是当粒子接近结束时再施加淡出效果会更棒。当粒子刚刚生成时,你想让它完全可见,当达到relAge = 1时,你想让它完全透明。

    要实现这个效果,你可以使用线性减少,但是如果使用图3-23左图中的曲线效果会更好。这次,你想从1到0,所以无需除以2:

    float alpha = 1-relAge*relAge; 

    现在就可以定义粒子的调制颜色了:

    Output.Color = float4(0.5f,0.5f,0.5f,alpha); 

    在pixel shader中,你将每个像素的颜色乘以这个颜色。这会将像素的alpha值调整为这里计算的值,这样粒子就会慢慢变得透明。颜色的RGB分量也通过因子2柔化,不然你会很容易看到独立的粒子。

    别忘了将纹理坐标传递到pixel shader,这样才能知道billboard的顶点对应纹理的哪个顶点:

    Output.TexCoord = texCoords; 
    return Output; 
    Pixel Shader

    幸运的是,pixel shader相对于vertex shader来说很简单:

    ExpPixelToFrame ExplosionPS(ExpVertexToPixel PSIn) : COLOR0 
    {
        ExpPixelToFrame Output = (ExpPixelToFrame)0; 
        
        Output.Color = tex2D(textureSampler, PSIn.TexCoord)*PSIn.Color; 
        
        return Output; 
    } 

    对每个像素,你从纹理中获取对应的颜色,将这个颜色乘以在vertex shader计算的调制颜色。这个调制颜色会减少亮度,对应粒子的生存时间设置alpha值。

    下面是technique定义:

    technique Explosion 
    {
        pass Pass0 
        {
            VertexShader = compile vs_1_1 ExplosionVS(); 
            PixelShader = compile ps_1_1 ExplosionPS(); 
        }
    } 
    在XNA中设置Technique参数

    别忘了在XNA代码中设置所有XNA-to-HLSL变量:

    expEffect.CurrentTechnique = expEffect.Techniques["Explosion"];
    expEffect.Parameters["xWorld"].SetValue(Matrix.Identity); 
    expEffect.Parameters["xProjection"].SetValue(quatMousCam.ProjectionMatrix); 
    expEffect.Parameters["xView"].SetValue(quatMousCam.ViewMatrix); 
    expEffect.Parameters["xCamPos"].SetValue(quatMousCam.Position); 
    expEffect.Parameters["xExplosionTexture"].SetValue(myTexture); 
    expEffect.Parameters["xCamUp"].SetValue(quatMousCam.UpVector); 
    expEffect.Parameters["xTime"]. SetValue((float)gameTime.TotalGameTime.TotalMilliseconds); 
    Additive Blending(附加混合)

    因为这个technique依赖于混合,你需要在绘制前设置绘制状态,alpha混合的更多信息可参加教程2-12和3-3。

    在本例中,你想使用additive blending让所有的颜色都叠加在一起。这可以通过设置渲染状态实现:

    device.RenderState.AlphaBlendEnable = true; 
    device.RenderState.SourceBlend = Blend.SourceAlpha; 
    device.RenderState.DestinationBlend = Blend.One; 

    第一行代码开启alpha混合。对每个像素,它的颜色决定于以下规则:

    finalColor=sourceBlend*sourceColor+destBlend*destColor 

    根据前面设置的渲染状态,这个规则会变成以下公式:

    finalColor=sourceAlpha*sourceColor+1*destColor 

    显卡会绘制每个像素多次,因为你将绘制大量的爆炸图像(需要关闭写入depth buffer)。所以当显卡已经绘制了80个粒子中的79个,并开始绘制最后一个时,会对这个粒子的每个像素进行以下操作:

    1.找到存储在frame buffer中的像素的当前颜色,将三个颜色通道乘以1(对应前面规则中的1*destColor)。

    2.获取在pixel shader中计算的这个粒子的新颜色,将这个颜色的三个通道乘以alpha通道,alpha通道取决于粒子的当前生存时间(对应上述规则中的sourceAlpha*sourceColor)。

    3. 将两个颜色相加,将结果保存到frame buffer中。

    在粒子的开始时刻(relAge=0),sourceAlpha等于1,所以additive blending会产生一个大爆炸。在粒子的结束时刻(relAge=1),newAlpha为0,粒子不产生效果。

    注意:这个混合规则的第二个例子可参加教程2-12。

    关闭写入到Depth Buffer

    你还要考虑到最后一个方面。当离开相机最经的粒子首先被绘制时,所有在它之后的粒子将不会被绘制(这会导致不发生混合)!所以绘制粒子时,你需要关闭z-buffer测试,并在将粒子作为场景中的最后一个元素被绘制。但这并不是一个好方法,因为当爆炸离开相机很远时且相机和爆炸之间有一个建筑物时,爆炸仍会被绘制,就好像之间没有这个建筑物似的!

    更好的方法是先绘制场景。然后关闭z-buffer写入将粒子作为最后一个元素进行绘制。通过这个方法,你可以解决这个问题:

    • 每个粒子的每个像素都会与z-buffer中的当前内容(包含深度信息)作比较。如果在相机和爆炸之间已经有一个物体被绘制了,那么爆炸的粒子就通不过z-buffer测试,不会被绘制。
    • 因为粒子不会改变z-buffer,就算第二个粒子在第一个粒子之后也会被绘制(当然,是在相机和爆炸间没有另一个物体的情况下)。

    下面是代码:

    device.RenderState.DepthBufferWriteEnable = false; 

    然后,绘制爆炸使用的三角形:

    expEffect.Begin(); 
    
    foreach (EffectPass pass in expEffect.CurrentTechnique.Passes) 
    {
        pass.Begin(); 
        device.VertexDeclaration = new VertexDeclaration(device, VertexExplosion.VertexElements); 
        device.DrawUserPrimitives<VertexExplosion> (PrimitiveType.TriangleList, explosionVertices, 0, explosionVertices.Length / 3); 
        pass.End(); 
    }
    expEffect.End(); 

    在绘制完爆炸后,你需要再次开启z-buffer写入:

    device.RenderState.DepthBufferWriteEnable = true; 

    当你想开始一个新的爆炸时,调用CreateExplosionVertices方法!本例中,当用户按下空格键时会调用这个方法:

    if ((keyState.IsKeyDown(Keys.Space))) 
                CreateExplosionVertices((float) gameTime.TotalGameTime.TotalMilliseconds);
    代码

    下面是生成爆炸所需顶点的方法:

    private void CreateExplosionVertices(float time) 
    {
        int particles = 80; 
        explosionVertices = new VertexExplosion[particles * 6]; 
        
        int i = 0; 
        for (int partnr = 0; partnr < particles; partnr++) 
        {
            Vector3 startingPos = new Vector3(5,0,0); 
            float r1 = (float)rand.NextDouble() - 0.5f; 
            float r2 = (float)rand.NextDouble() - 0.5f; 
            float r3 = (float)rand.NextDouble() - 0.5f; 
            Vector3 moveDirection = new Vector3(r1, r2, r3); 
            moveDirection.Normalize(); 
            
            float r4 = (float)rand.NextDouble(); 
            r4 = r4 / 4.0f * 3.0f + 0.25f; 
            
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 1, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 0, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 0, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(1, 1, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 1, time, 1000), new Vector4(moveDirection, r4)); 
            explosionVertices[i++] = new VertexExplosion(startingPos, new Vector4(0, 0, time, 1000), new Vector4(moveDirection, r4)); 
        }
    } 

    下面是完整的Draw方法:

    protected override void Draw(GameTime gameTime) 
    {
        device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1, 0); 
        cCross.Draw(quatCam.ViewMatrix, quatCam.ProjectionMatrix); 
        
        if (explosionVertices != null) 
        {
            //draw billboards 
            expEffect.CurrentTechnique = expEffect.Techniques["Explosion"]; 
            expEffect.Parameters["xWorld"].SetValue(Matrix.Identity); 
            expEffect.Parameters["xProjection"].SetValue(quatCam.ProjectionMatrix); 
            expEffect.Parameters["xView"].SetValue(quatCam.ViewMatrix); 
            expEffect.Parameters["xCamPos"].SetValue(quatCam.Position); 
            expEffect.Parameters["xExplosionTexture"].SetValue(myTexture); 
            expEffect.Parameters["xCamUp"].SetValue(quatCam.UpVector); 
            expEffect.Parameters["xTime"]. SetValue((float)gameTime.TotalGameTime.TotalMilliseconds); 
            
            device.RenderState.AlphaBlendEnable = true; 
            device.RenderState.SourceBlend = Blend.SourceAlpha; 
            device.RenderState.DestinationBlend = Blend.One; 
            device.RenderState.DepthBufferWriteEnable = false; 
            
            expEffect.Begin(); 
            foreach (EffectPass pass in expEffect.CurrentTechnique.Passes) 
            {
                pass.Begin(); 
                device.VertexDeclaration = new VertexDeclaration(device, VertexExplosion.VertexElements); 
                device.DrawUserPrimitives<VertexExplosion> (PrimitiveType.TriangleList, explosionVertices, 0, explosionVertices.Length / 3); 
                pass.End(); 
            }
            expEffect.End(); 
            
            device.RenderState.DepthBufferWriteEnable = true; 
        }
        base.Draw(gameTime); 
    } 

    下面是vertex shader代码:

    ExpVertexToPixel ExplosionVS(float3 inPos: POSITION0, float4 inTexCoord: TEXCOORD0, float4 inExtra: TEXCOORD1) 
    {
        ExpVertexToPixel Output = (ExpVertexToPixel)0; 
        
        float3 startingPosition = mul(inPos, xWorld); 
        float2 texCoords = inTexCoord.xy; 
        float birthTime = inTexCoord.z; 
        float maxAge = inTexCoord.w; 
        float3 moveDirection = inExtra.xyz; 
        float random = inExtra.w; 
        
        float age = xTime - birthTime; 
        float relAge = age/maxAge; 
        float sizer = saturate(1-relAge*relAge/2.0f); 
        float size = 5.0f*random*sizer; 
        
        float totalDisplacement = sin(relAge*6.28f/4.0f)*3.0f*random; 
        float3 billboardCenter = startingPosition + totalDisplacement*moveDirection; 
        billboardCenter += age*float3(0,-1,0)/1000.0f; 
        float3 finalPosition = BillboardVertex(billboardCenter, texCoords, size); 
        float4 finalPosition4 = float4(finalPosition, 1); 
        
        float4x4 preViewProjection = mul (xView, xProjection); 
        Output.Position = mul(finalPosition4, preViewProjection); 
        
        float alpha = 1-relAge*relAge; 
        Output.Color = float4(0.5f,0.5f,0.5f,alpha); 
        
        Output.TexCoord = texCoords; 
        
        return Output; 
    } 

    vertex shader使用前面定义的BillboardVertex方法,在教程的最后你还可以看到简单的pixel shader和technique定义。

    3

    译者注:在XNA官方网站上http://creators.xna.com/en-US/sample/particle3d也有一个粒子系统的例子,不过它的实现方法是使用点精灵(point sprite),这个方法没有使用billboard那样具有弹性,但是它的优点是只使用一个顶点就可以绘制一个粒子,而在billboard需要六个,这可以减少发送到显卡的数据量。

  • 相关阅读:
    【AGC016E】Poor Turkeys
    【51nod 1597】有限背包计数问题
    RPA应用场景-营业收入核对
    RPA应用场景-报税机器人
    UiPath保存图片操作的介绍和使用
    UiPath图片操作截图的介绍和使用
    UiPath存在图像Image Exists的介绍和使用
    UiPath存在元素Element Exists的介绍和使用
    UiPath存在文本Text Exists的介绍和使用
    UiPath文本操作Get Visible Text的介绍和使用
  • 原文地址:https://www.cnblogs.com/AlexCheng/p/2120147.html
Copyright © 2020-2023  润新知