Xbox LIVE Indie Games
Sort Discussions: Previous Discussion Next Discussion
Page 1 of 1 (3 posts)

Adding support for soft particles to the 3d particle sample

Last post 5/18/2009 6:59 PM by Phil Fortier. 2 replies.
  • 5/16/2009 11:59 PM

    Adding support for soft particles to the 3d particle sample

    Since it took me a while to figure all this out, I thought I'd write something up.  It's not a complete sample (it will take some fiddling to integrate with whatever you are doing, and I don't show the CPU code for generating the depth texture for example), but it shows how I modified the 3d particle sample (in the education section) to support soft particles, and have it work on the Xbox. 

    Soft particles greatly improve the look of a particle engine - no more hard edges where the particles intersect solid geometry in the scene.

    The main challenges were:
    - getting proper depth information for a particle in the pixel shader for point sprites
    - figuring out a replacement for VPOS (in order to sample the depth texture), since VPOS isn't useful when predicated tiling is triggered on the Xbox

    First I create a depth texture using SurfaceFormat.Single (32 bit floating point), which I render by doing an additional pass on my solid geometry.  I store a z value in the texture which is the z value from the POSITION semantic, scaled to fit in 0-1.  I use an arbitrary number to scale it - the furthest distance I expect to have in my scene.

    struct VS_DEPTH_OUTPUT  
        float4 Position : POSITION;  
        float Depth : TEXCOORD0;  
    VS_DEPTH_OUTPUT RenderDepthMapVS(float3 position: POSITION)  
        VS_DEPTH_OUTPUT output;  
        //generate the world-view-projection matrix  
        float4x4 wvp = mul(mul(world, view), projection);  
        //transform the input position to the output  
        output.Position = mul(float4(position, 1.0), wvp);  
        output.Depth = output.Position.z;  
        return output;    
    #define MAXDEPTH 100  
    float4 RenderDepthMapPS(float2 input : TEXCOORD0) : COLOR  
        float depth = input.x / MAXDEPTH;  
        return float4(depth, 0, 0, 1); // in red channel  

    So I now have a 32bit floating point depth texture, with values from 0 to 1.

    In the particle sample, I add a bunch of extra information into another COLOR semantic.  This appears to have sufficient precision on the Xbox, but may not on PC graphics cards (apparently 8bit precision is common, which wouldn't be enough).  It's possible some PC graphics cards may not support an additional COLOR semantic too?

    struct VertexShaderOutput  
        float4 Position : POSITION0;  
        float4 Color : COLOR0;  
        float4 Rotation : COLOR1;  
        float Size : PSIZE0;  
        float4 ExtraInfo : COLOR2;  // x: depth(z)  
                                    // y: pointsize  
                                    // zw: xy screenpos (relative to (0,0)-(1,1)) for center of particle  

    And to fill these in, I do something like this in the vertex shader:

        // Fill in the extra info for soft particles.  
        output.ExtraInfo.x = output.Position.z; // Same depth across the particle  
        // Ok to divide by w here, since there is no interpolation (and flip the y coord)  
        float2 screenPosition = (output.Position.xy / output.Position.w) * float2(0.5, -0.5) + float2(0.5, 0.5);     // screenPosition is relative to topleft:(0,0) to bottomRight:(1,1)  = screenPosition;  
        output.ExtraInfo.y = output.Size; 

    To figure out the screen position for the point sprite:
    - I have the screen position of the center of the sprite ( above).
    - I know the size of the point sprite in pixels (ExtraInfo.y above)
    - I know the texture coords go from (0,0) to (1,1) from topleft to bottom right
    - Using this info, I can calculate the screen position of the pixel being rendered, and use this position to know where to sample from the depth texture.  I was worried about rounding (I'm not sure how the rasterizer handles the PSIZE semantic) and maybe having some off-by-one errors.  But I haven't noticed anything bad yet.

    Here is the relevant parts of the pixel shader, showing how I:
    - calculate the screen position (to make up for the VPOS semantic I can't use)
    - use this to sample the depth texture, and fade the particle alpha to zero as the particle is closer to solid geometry in the scene.

    float2 ViewportSize; // This is an effect parameter you supply  
    // Pixel shader input structure for particles that do not rotate.  
    struct NonRotatingPixelShaderInput  
        float4 Color : COLOR0;
    #ifdef XBOX  
        float2 TextureCoordinate : SPRITETEXCOORD;
        float2 TextureCoordinate : TEXCOORD0;
        float4 ExtraInfo : COLOR2;  
    texture DepthTexture;
    #define MAXDEPTH 100  
    sampler depthSampler = sampler_state  
        Texture = (DepthTexture);  
        MinFilter = Point;  
        MagFilter = Point;  
        MipFilter = Point;  
        AddressU = Clamp;  
        AddressV = Clamp;  
    #define CONTRAST_POWER 1.2  
    #define FADE_DISTANCE 1     // Distance in world coordinates over which to fade the particle from full to 0.  
    // pixelPosition: screen coordinate in pixels  
    // particleDepth: depth of particle in world space  
    float CalculateZFade(float2 pixelPosition, float particleDepth)  
        float zFade = 1;  
        float2 screenUV = pixelPosition / ViewportSize;  
        float sceneDepth = tex2D(depthSampler, screenUV).x;   
        if (sceneDepth != 0) // I take 0 to mean depth wasn't set, though you could also clear your depth texture to some value that   
    makes sense  
            sceneDepth *= MAXDEPTH; // Expand back out from 0-1 to actual world coordinates  
            float input = ((sceneDepth - particleDepth) / FADE_DISTANCE);  
            if ((input < 1) && (input > 0))  
                // Make it fade smoothly from 0 and 1 - I think I grabbed this curve from some nVidia paper  
                zFade = 0.5 * pow(saturate(2*((input > 0.5) ? (1 - input) : input)), CONTRAST_POWER);  
                zFade = (input > 0.5) ? (1 - zFade) : zFade;  
                zFade = saturate(input);  
        return zFade;  
    // Calculates pixel position (0,0 to ViewportSize) given:  
    //  - the center position of the particle  
    //  - the particle size (in pixels)  
    //  - the current texture coordinate  
    float2 CalculateParticlePixelPosition(float2 centerPosition, float size, float2 texCoord)  
        centerPosition *= ViewportSize;  
        // From here on, everything is in pixels  
        float2 offset = texCoord * float2(size, size);  
        float2 upperLeft = centerPosition - size / 2;  
        return upperLeft + offset;  
    float4 FadeParticleAlpha(float4 color, float depth, float2 pixelPosition)  
        float zFade = CalculateZFade(pixelPosition, depth);  
        // I seem to get better results by fading each component (for emmissive particles)  
        return float4(color.r * zFade, color.g * zFade, color.b * zFade, color.a * zFade);  
    // Pixel shader for drawing particles that do not rotate, modified for soft particles  
    float4 NonRotatingPixelShader(NonRotatingPixelShaderInput input) : COLOR0  
        float2 pixelPosition = CalculateParticlePixelPosition(, input.ExtraInfo.y, input.TextureCoordinate);  
        return FadeParticleAlpha(tex2D(Sampler, input.TextureCoordinate) * input.Color, input.ExtraInfo.x, pixelPosition);  

    Hope this helps someone!

  • 5/18/2009 2:05 PM In reply to

    Re: Adding support for soft particles to the 3d particle sample

    Wow cheers mate - will definately take a look at this later : ) 
    Any idea of what kind of extra performance (if any) it requires from your game and hardware? 
  • 5/18/2009 6:59 PM In reply to

    Re: Adding support for soft particles to the 3d particle sample

    Well, it means you have to render your solid geometry a 2nd time, and switch out render targets.
    And the previously-very-simple particle pixel shader gets significantly longer.  So there is definitely some performance impact.

    I haven't noticed any slowdowns, but I am not GPU limited.

    Here is a example of before and after:
    Comparison of before and after
Page 1 of 1 (3 posts) Previous Discussion Next Discussion