Creating explosions within the HiDef profile
One of the more visceral elements of a good explosion is the shockwave that races ahead of the flames. Extracting and modifying one of the standard Microsoft examples of distortion mapping can let us enjoy the rather gratifying joy of shockwave-laden explosions in our own games.
Getting ready
This recipe assumes you have access to a spherical mesh and some sort of flame texture. It was originally written with the sphere generator presented in the Modeling spheres recipe of Chapter 3, Procedural Modeling, but should work equally well with any other method of creating spheres. Where you find the call to a CreateSphere()
method in the following example code, feel free to replace that with your own.
Likewise, any references to GeometricBuffer
classes of Chapter 3, Procedural Modeling can be substituted for any other mesh container style classes.
How to do it...
To create an explosion within the HiDef profile:
1. Add a new effect file to your game's content project, named
DistortionGenerator.fx
.2. Inside the new file, clear any existing content and add the input parameters:
float4x4 WorldViewProjection; float4x4 WorldView; float DistortionScale;
3. Define the structure of the data that passes between the vertex and pixel shaders:
struct PositionNormal { float4 Position : POSITION; float3 Normal : NORMAL; }; struct PositionDisplacement { float4 Position : POSITION; float2 Displacement : TEXCOORD; };
4. Add a vertex shader that calculates the distortion on behalf of the pixel shader based upon a vertex's normal:
PositionDisplacement PullIn_VertexShader(PositionNormal input) { PositionDisplacement output; output.Position = mul(input.Position, WorldViewProjection); float3 normalWV = mul(input.Normal, WorldView); normalWV.y = -normalWV.y; float amount = dot(normalWV, float3(0,0,1)) * DistortionScale; output.Displacement = float2(.5,.5) + float2(amount * normalWV.xy); return output; }
5. Next, add the corresponding pixel shader that emits the distorted normal as a color:
float4 DisplacementPassthrough_PixelShader(float2 displacement : TEXCOORD) : COLOR { return float4(displacement, 0, 1); }
6. Complete the file by tying the vertex and pixel shader together into a technique:
technique PullIn { pass { VertexShader = compile vs_2_0 PullIn_VertexShader(); PixelShader = compile ps_2_0 DisplacementPassthrough_PixelShader(); } }
7. Create a new effect file named
DistortionApplicator.fx
and add it to your content project.8. Specify the effect's inputs to receive two textures and the weighting arrays for a Gaussian blur:
sampler SceneTexture : register(s0); sampler DistortionMap : register(s1); #define SAMPLE_COUNT 15 float2 SampleOffsets[SAMPLE_COUNT]; float SampleWeights[SAMPLE_COUNT];
9. Add a constant to help in the translation of a zero amount when moving between 0-1 and 0-255 ranges:
const float ZeroOffset = 0.5f / 255.0f;
10. Include a pixel shader that renders a distorted and possibly blurred version of the scene texture if the distortion map texture is any color other than black:
float4 Distort_PixelShader(float2 TexCoord : TEXCOORD0, uniform bool distortionBlur) : COLOR0 { float2 displacement = tex2D(DistortionMap, TexCoord).rg; float4 finalColor = 0; if ((displacement.x == 0) && (displacement.y == 0)) { finalColor = tex2D(SceneTexture, TexCoord); } else { displacement -= .5 + ZeroOffset; if (distortionBlur) { for (int i = 0; i < SAMPLE_COUNT; i++) { finalColor += tex2D( SceneTexture, TexCoord.xy + displacement + SampleOffsets[i]) * SampleWeights[i]; } } else { finalColor = tex2D(SceneTexture, TexCoord.xy + displacement); } } return finalColor; }
11. Add two techniques to allow the pixel shader to be used with or without blurring:
technique Distort { pass { PixelShader = compile ps_2_0 Distort_PixelShader(false); } } technique DistortBlur { pass { PixelShader = compile ps_2_0 Distort_PixelShader(true); } }
12. In your game project, create a new factory class to produce explosions:
class HiDefExplosionFactory {
13. Add a static method to create new explosions:
public static HiDefExplosion Create( GraphicsDevice graphicsDevice, ContentManager content) { var buffer = CreateSphere(graphicsDevice); var occlusionEffect = new BasicEffect(graphicsDevice) { DiffuseColor = Color.Black.ToVector3() }; var flameEffect = new BasicEffect(graphicsDevice) { DiffuseColor = Color.White.ToVector3(), Texture = content.Load<Texture2D>("explosion/lava"), TextureEnabled = true, Alpha = 0.5f }; var distortionGeneratorEffect = content.Load<Effect>("explosion/DistortionGenerator"); var distortionApplicatorEffect = content.Load<Effect>("explosion/DistortionApplicator"); return new HiDefExplosion( graphicsDevice, buffer, occlusionEffect , flameEffect, distortionGeneratorEffect, distortionApplicatorEffect); }
14. Create a new Explosion class:
class HiDefExplosion {
15. Add the instance-level variables that will be used to render the flames:
GeometricBuffer<VertexPositionNormalTexture> buffer; BasicEffect flameEffect; RenderTarget2D sceneRenderTarget;
16. Declare the instance-level variables that will be used to render the shockwave:
RenderTarget2D distortionRenderTarget; public BasicEffect OcclusionEffect; Effect distortionGeneratorEffect; Effect distortApplicatorEffect;
17. Append the instance-level variables used to display the completed animation:
SpriteBatch spriteBatch; float explosionLifeSpanSeconds; Curve sizeCurve; Curve flameAlphaCurve; public Matrix World; private bool exploding; private double explosionStartTime; private double explosionEndTime; private float flameAlpha; private const float blurAmount = 1.25f;
18. Add a constructor:
public HiDefExplosion( GraphicsDevice graphicsDevice, GeometricBuffer<VertexPositionNormalTexture> buffer, BasicEffect occlusionEffect, BasicEffect flameEffect, Effect distortersEffect, Effect distortEffect) {
19. Inside the constructor, populate the instance- level variables:
this.buffer = buffer; OcclusionEffect = occlusionEffect; this.flameEffect = flameEffect; this.distortionGeneratorEffect = distortersEffect; this.distortApplicatorEffect = distortEffect;
20. Set up the pieces required to capture and display the various elements of the explosion:
spriteBatch = new SpriteBatch(graphicsDevice); var pp = graphicsDevice.PresentationParameters; sceneRenderTarget = new RenderTarget2D(graphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat); distortionRenderTarget = new RenderTarget2D(graphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat);
21. Populate the Gaussian blur weight arrays of the distortion effect:
SetBlurEffectParameters( 1f / (float)pp.BackBufferWidth, 1f / (float)pp.BackBufferHeight);
22. Create the timings for the explosion animation:
explosionLifeSpanSeconds = 5f; sizeCurve = new Curve(); sizeCurve.Keys.Add(new CurveKey(0, 0.1f)); sizeCurve.Keys.Add(new CurveKey(0.75f, 5f)); flameAlphaCurve = new Curve(); flameAlphaCurve.Keys.Add(new CurveKey(0, 0f)); flameAlphaCurve.Keys.Add(new CurveKey(0.05f, 1f)); flameAlphaCurve.Keys.Add(new CurveKey(0.15f, 0f));
23. Add a method to begin capturing a scene into the
sceneRenderTarget:
public void BeginSceneCapture(GraphicsDevice graphicsDevice) { graphicsDevice.SetRenderTarget(sceneRenderTarget); }
24. Create a method to render the flame as a final element onto the scene, before switching the rendering from the
sceneRenderTarget
back to the screen:public void EndSceneCapture( GraphicsDevice graphicsDevice, Matrix view, Matrix projection) { if (exploding) { flameEffect.View = view; flameEffect.Projection = projection; flameEffect.World = World; flameEffect.Alpha = flameAlpha; // draw explosion particle. // e.g. here's how it would be done using // Chapter 3's GeometricBuffer classes buffer.IsTextureTransparent = true; buffer.Draw(flameEffect); } graphicsDevice.SetRenderTarget(null); }
25. Next up is the method to begin capturing any objects within a scene that may be occluding the explosion from the player:
public void BeginOcclusionCapture( GraphicsDevice graphicsDevice) { if (!exploding) { return; } graphicsDevice.SetRenderTarget(distortionRenderTarget); graphicsDevice.Clear(Color.Black); }
26. Add the method to render the distortion effect into a render target, and shift the rendering to the screen once more:
public void EndOcclusionCapture( GraphicsDevice graphicsDevice, Matrix view, Matrix projection) { if (!exploding) { return; } Matrix meshWorldView = Matrix.CreateScale(1.5f) * World * view; distortionGeneratorEffect.CurrentTechnique = distortionGeneratorEffect.Techniques["PullIn"]; distortionGeneratorEffect.Parameters["WorldView"].SetValue(meshWorldView); distortionGeneratorEffect.Parameters["WorldViewProjection"].SetValue( meshWorldView * projection); distortionGeneratorEffect.Parameters["DistortionScale"].SetValue( 0.0125f); buffer.Draw(distortionGeneratorEffect); graphicsDevice.SetRenderTarget(null); }
27. Specify the method and the associated variables to start the animation:
public void Explode(GameTime gameTime) { exploding = true; explosionStartTime = gameTime.TotalGameTime.TotalSeconds; explosionEndTime = explosionStartTime + explosionLifeSpanSeconds; }
28. Insert an
Update()
method to play the animation once it has begun:public void Update(GameTime gameTime) { if (!exploding) { return; } if (gameTime.TotalGameTime.TotalSeconds >= explosionEndTime) { exploding = false; return; } var explosionTimeOffset = gameTime.TotalGameTime.TotalSeconds - explosionStartTime; World = Matrix.CreateScale(sizeCurve.Evaluate((float)explosionTimeOffset)) * Matrix.CreateTranslation(Vector3.Zero); flameAlpha = flameAlphaCurve.Evaluate((float)explosionTimeOffset); }
29. Add the
Draw()
method to render the scene along with any explosion that may be in progress:public void Draw(GraphicsDevice graphicsDevice) { if (exploding) { spriteBatch.Begin(0, BlendState.Opaque, null, null, null, distortApplicatorEffect); distortApplicatorEffect.CurrentTechnique = distortApplicatorEffect.Techniques["DistortBlur"]; graphicsDevice.Textures[1] = distortionRenderTarget; graphicsDevice.SamplerStates[1] = SamplerState.PointClamp; } else { spriteBatch.Begin(); } var viewport = graphicsDevice.Viewport; spriteBatch.Draw( sceneRenderTarget, new Rectangle(0, 0, viewport.Width, viewport.Height), Color.White); spriteBatch.End(); }
30. Finish the class with methods to calculate Gaussian blur weightings:
void SetBlurEffectParameters(float dx, float dy) { EffectParameter weightsParameter, offsetsParameter; weightsParameter = distortApplicatorEffect. Parameters["SampleWeights"]; offsetsParameter = distortApplicatorEffect. Parameters["SampleOffsets"]; int sampleCount = weightsParameter.Elements.Count; float[] sampleWeights = new float[sampleCount]; Vector2[] sampleOffsets = new Vector2[sampleCount]; sampleWeights[0] = ComputeGaussian(0); sampleOffsets[0] = new Vector2(0); float totalWeights = sampleWeights[0]; for (int i = 0; i < sampleCount / 2; i++) { float weight = ComputeGaussian(i + 1); sampleWeights[i * 2 + 1] = weight; sampleWeights[i * 2 + 2] = weight; totalWeights += weight * 2; float sampleOffset = i * 2 + 1.5f; var delta = new Vector2(dx, dy) * sampleOffset; sampleOffsets[i * 2 + 1] = delta; sampleOffsets[i * 2 + 2] = -delta; } HiDef profileexplosions, creatingfor (int i = 0; i < sampleWeights.Length; i++) { sampleWeights[i] /= totalWeights; } weightsParameter.SetValue(sampleWeights); offsetsParameter.SetValue(sampleOffsets); } static float ComputeGaussian(float n) { return (float)((1.0 / Math.Sqrt(2 * Math.PI * blurAmount)) * Math.Exp(-(n * n) / (2 * blurAmount * blurAmount))); }
How it works...
I find it easiest to imagine how this recipe works in terms of how I might try to achieve a similar effect using more traditional real-world artistic techniques.
For example, if this was an airbrushed artwork, I might paint the surrounding scene first, apply masking tape to any area where I didn't want the explosion to appear, spray a fiery design over the top, remove the tape, and voila! A scene with an explosion is produced.
Between the BeginSceneCapture()
and EndSceneCapture()
methods is where we draw the surrounding scene.
Next, we create a mask by painting the shape of the explosion amongst a completely blackened version of our scene, through calling the BeginOcclusionCapture()
and EndOcclusionCapture()
methods, and the DistortionGenerator
effect.
Rendering this "heat map" amongst a blackened version of the scene means that any elements of the scene that would normally obscure the explosion are rendered as black, over the top of the explosion shape, thereby masking those portions out.
The color we fill the explosion shape in with is not the final color of the rendered explosion though. Instead, it is a sort of a "heat map", indicating the direction and amount of distorted explosive effect that will be applied to each pixel in the final image.
Inside the Draw()
method is where we bring all the elements together with the help of the DistortionApplicator
effect.
Taking note of which pixels are black and which are not within the masked image, the DistortionApplicator
effect renders the original scene image with the appropriate amount of distortion in each area, thereby achieving the final result: a scene with an explosive bubble of distortion flooding across the landscape.
There's more...
The draw, mask, and combine technique demonstrated in this recipe is the foundation of a vast array of effects found in games.
Apply the distortions to a flattened pane and you're half way towards producing a patch of water. Change the intensity of the distortion based upon the depth of the original scene, and you're very close to a passable focal blur effect.
See also...
Rendering water within the HiDef profile recipe in Chapter 4, Creating Water and Sky.