Monday, 9 July 2012

How to do Old School Fire Part 2: GPU Flames


I am sure you are more than capable of taking the CPU flames and doing the calculations on the GPU, but if either you couldn’t be bothered or are un familiar with shaders, then this is the post for you.




GPU Fire

As before we have a color palette and a fire texture, but this time we are going to use render targets and a shader with two techniques in it.

In the constructor the color palette is constructed as before.

protected void BuildPalette()
{
for (int x = 0; x < palette.Length; x++)
palette[x] = new Color(255, Math.Min(255, x * 2), x / 3, x * 5);
}

In the LoadContent we set up the textures and render target

protected override void LoadContent()
{
base.LoadContent();
fire = new Texture2D(GraphicsDevice, Resolution.X, Resolution.Y);
Color[] f = new Color[fire.Width * fire.Height];
for (int c = 0; c < f.Length; c++)
f[c] = Color.Black;
fire.SetData<Color>(f);
fireMap = new Texture2D(GraphicsDevice, Resolution.X, Resolution.Y);
int[] i = new int[f.Length];
fireMap.SetData<int>(i);
p = new Texture2D(GraphicsDevice, 256, 1);
p.SetData<Color>(palette);
effect = Game.Content.Load<Effect>("Shaders/OldSchoolFire");
rt = new RenderTarget2D(GraphicsDevice, Resolution.X, Resolution.Y);
rtc = new Color[Resolution.X * Resolution.Y];
}

And then we have the super funky draw call

public override void Draw(GameTime gameTime)
{
float[] d = new float[fireMap.Width * fireMap.Height];
fireMap.GetData<float>(d);
for (int x = 0; x < fireMap.Width; x++)
{
for (int y = fireMap.Height - 1; y < fireMap.Height; y++)
d[x + (y * fireMap.Width)] = MathHelper.Clamp((float)rand.NextDouble(), minFuel / 255f, maxFuel / 255f);
}
GraphicsDevice.Textures[1] = null;
fireMap.SetData<float>(d);
effect.Parameters["fireMap"].SetValue(fireMap);
effect.Parameters["oxygen"].SetValue(oxygen);
effect.Parameters["paletteMap"].SetValue(p);
effect.Parameters["width"].SetValue(Resolution.X);
effect.Parameters["height"].SetValue(Resolution.Y);
// Capture fireMap data.
GraphicsDevice.SetRenderTarget(rt);
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend);
effect.CurrentTechnique = effect.Techniques["UpdateFire"];
effect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Draw(fire, new Rectangle(0, 0, Resolution.X, Resolution.Y), Color.White);
spriteBatch.End();
GraphicsDevice.SetRenderTarget(null);
// Get the data from the rt and pack it back into the map.
rt.GetData<Color>(rtc);
GraphicsDevice.Textures[1] = null;
fireMap.SetData<Color>(rtc);
// Re draw with color passing in the firemap data
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied);
effect.CurrentTechnique = effect.Techniques["ColorFire"];
effect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Draw(fire, new Rectangle(0, 0, GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight), Color.White);
spriteBatch.End();
// Debug
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque);
spriteBatch.Draw(p, new Rectangle(GraphicsDevice.PresentationParameters.BackBufferWidth - 128, 0, 128, 128), Color.White);
spriteBatch.Draw(fireMap, new Rectangle(GraphicsDevice.PresentationParameters.BackBufferWidth - 256, 0, 128, 128), Color.White);
spriteBatch.End();
}

So, this is where all the action is, first we construct the fire map, taking the current texture and randomizing the base pixels (could probably move this to the GPU too), then load the parameters of our shader. Now we need to give our fire map to the GPU so it can work it’s magic on the texture, so we set up the render target, clear the device, render our fire texture using the shader calling the “UpdateFire” technique.

// Fire Map function
float4 UpdateFireMap(VertexShaderOutput input) : COLOR0
{
float col = 0;
// Calculate the size of a pixel in texture space
float2 pix = float2(1,1) / float2(width,height);
// Sample the pixels below me.
col += tex2D(map,float2(input.TexCoord.x - pix.x,input.TexCoord.y + pix.y));
col += tex2D(map,float2(input.TexCoord.x,input.TexCoord.y + (pix.y * 2)));
col += tex2D(map,float2(input.TexCoord.x + pix.x,input.TexCoord.y + pix.y));
col += tex2D(map,float2(input.TexCoord.x,input.TexCoord.y + (pix.y * 2)));
// Divied the sum by the 4 + the oxygen mod.
col /= (4 + oxygen);
// return the map value;
return float4(col,col,col,1);
}

And in here we are just doing what we did on the CPU before. Once we get this texture back off the GPU, we can then use the “ColorFire” technique to render the flame.

// Flame color function
float4 ColorMap(VertexShaderOutput input) : COLOR0
{
// Get this pixels map value and color it based on the palette
float col = tex2D(map,input.TexCoord);
// Write it out to the screen :D
float4 pcol = tex2D(palette,float2(col,0));
return pcol;
}

And there you have GPU old school flames.

But what of the spinning cube!? I hear you shout…




Works as before, but the fire map is derived from the scene and I simply color the box lines to that of the level of the oxygen value. I have also added in some text as an added effect. So with this you can set anything you like on fire :)

You can get the sample code here for both the GPU and CPU fire effects.

If you want to see this effect running on your shiny WP7, then you can dld this source here to do that.

3 comments:

  1. can you please reupload the sample project of the gpu version? thanks :)

    ReplyDelete
    Replies
    1. Hi HellGate :)

      I will, ill try and get the link fixed on this post in the next 24 hours :)

      Delete