You can also find my blog on Tumblr
This semester I took a very interesting Computer Graphics class (CPI 411: Graphics for Games at Arizona State University), and had to pick a topic from one of the GPU Gems books and implement it for my final project.
That was the first time I attempted implementing something from the GPU Gems and I realized that the book assumes you know how to implement a lot of “basic” things that are required for the shader, and as a beginner in Computer Graphics, that took me a while since not everything works as advertised!
In this post, I describe how I implemented “Motion Blur as a Post-Processing Effect” from GPU Gems 3, Chapter 27, using MonoGame 3.5 and HLSL.
public class Game1 : Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont font;
Effect blurShader, lightShader;
float cameraAngleY = 120, cameraAngleX = 50; //camera rotation angles
float lightAngleY = 20, lightAngleX = 160; //light rotation angles
float lightDistance = 10;
float distance = 400; //camera distance
Vector3 cameraPosition, cameraTarget, lightPosition;
Matrix world = Matrix.Identity;
Matrix view = Matrix.CreateLookAt(
new Vector3(0, 0, 20),
new Vector3(0, 0, 0),
Vector3.UnitY);
Matrix projection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.ToRadians(45), //field of view
1024f / 768f,//aspect ratio
0.1f, //near (e.g 0.1f)
2000f); //far (e.g. 1000f)
Matrix worldViewProjection = Matrix.Identity;
Matrix preWorldViewProjection = Matrix.Identity; //WorldViewProjection from previous frame (used for blur)
MouseState preMouse; //previous mouse state
KeyboardState preKeyboard; //previous keyboard state
Model[] models;
Matrix[] modelTransform;
Texture2D depthMap;
Texture2D litScene;
RenderTarget2D litSceneRenderTarget, depthMapRenderTarget;
Matrix lightView = Matrix.CreateLookAt(new Vector3(0, 0, 10), Vector3.Zero, Vector3.UnitY);
Matrix lightProjection = Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver2, 1f, 1f, 100f
);
//For scene lighting
Vector4 ambient = new Vector4(0.5f, 0.5f, 0.5f, 1f);
Vector4 diffuseColor = new Vector4(0.5f, 0.5f, 0.5f, 1f);
float diffuseIntensity = 1.0f;
Vector4 specularColor = new Vector4(0.5f, 0.5f, 0.5f, 1f);
float specularIntensity = 1.0f;
float shininess = 40;
bool drawBlurred = true; //flag to switch between blur/no-blur
float preDeltaRotY = 0.0f;
float preDeltaRotX = 0.0f;
float preDeltaDistance = 0.0f;
float preDeltaDown = 0.0f;
float preDeltaRight = 0.0f;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.PreferredBackBufferHeight = 768;
graphics.PreferredBackBufferWidth = 1024;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load("Font");
models = new Model[] {
Content.Load("terrain/terrain"),
Content.Load("wolf/Wolf")
};
modelTransform = new Matrix[]
{
//Matrix.Identity*Matrix.CreateScale(0.1f),
Matrix.CreateTranslation(0,0,-50f),
Matrix.Identity
};
blurShader = Content.Load("BlurShader");
lightShader = Content.Load("PhongShader");
litSceneRenderTarget = new RenderTarget2D(GraphicsDevice, Window.ClientBounds.Width, Window.ClientBounds.Height, false, SurfaceFormat.Color, DepthFormat.Depth24, 0, RenderTargetUsage.PlatformContents);
depthMapRenderTarget = new RenderTarget2D(GraphicsDevice, Window.ClientBounds.Width, Window.ClientBounds.Height, false, SurfaceFormat.Color, DepthFormat.Depth24, 0, RenderTargetUsage.PlatformContents);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
//Switch between blur/no-blur with B key
if(Keyboard.GetState().IsKeyUp(Keys.B) && preKeyboard.IsKeyDown(Keys.B)) this.drawBlurred = !this.drawBlurred;
//Control light angles with Arrow Keys
if (Keyboard.GetState().IsKeyDown(Keys.Left)) lightAngleY += 0.02f;
if (Keyboard.GetState().IsKeyDown(Keys.Right)) lightAngleY -= 0.02f;
if (Keyboard.GetState().IsKeyDown(Keys.Up)) lightAngleX += 0.02f;
if (Keyboard.GetState().IsKeyDown(Keys.Down)) lightAngleX -= 0.02f;
//Rotation:
float deltaRotY = 0.0f;
float deltaRotX = 0.0f;
//Mouse Left button
if (Mouse.GetState().LeftButton == ButtonState.Pressed)
{
deltaRotY -= (Mouse.GetState().X - preMouse.X) / 100f;
deltaRotX += (Mouse.GetState().Y - preMouse.Y) / 100f;
}
//Keyboard: FTGH + shift/control to control speed
if (Keyboard.GetState().IsKeyDown(Keys.F)) deltaRotY -= 0.05f;
if (Keyboard.GetState().IsKeyDown(Keys.H)) deltaRotY += 0.05f;
if (Keyboard.GetState().IsKeyDown(Keys.G)) deltaRotX += 0.05f;
if (Keyboard.GetState().IsKeyDown(Keys.T)) deltaRotX -= 0.05f;
//Make rotation faster if Control/Shift pressed
if (Keyboard.GetState().IsKeyDown(Keys.LeftControl)){
deltaRotX *= 2;
deltaRotY *= 2;
}
if (Keyboard.GetState().IsKeyDown(Keys.LeftShift)){
deltaRotX *= 2;
deltaRotY *= 2;
}
cameraAngleX += deltaRotX + preDeltaRotX;
cameraAngleY += deltaRotY + preDeltaRotY;
//momentum
preDeltaRotX += deltaRotX;
preDeltaRotY += deltaRotY;
preDeltaRotX /= 20.0f;
preDeltaRotY /= 20.0f;
//Zoom
//Mouse Right button
float deltaDistance = 0.0f;
if (Mouse.GetState().RightButton == ButtonState.Pressed)
{
deltaDistance += (Mouse.GetState().X - preMouse.X) / 10f;
}
//Keyboard: Z (zoom out), X (zoom in)
if (Keyboard.GetState().IsKeyDown(Keys.Z)) deltaDistance -= 10.0f;
if (Keyboard.GetState().IsKeyDown(Keys.X)) deltaDistance += 10.0f;
//Make zoom faster if Control/Shift pressed
if (Keyboard.GetState().IsKeyDown(Keys.LeftControl)) deltaDistance *= 2;
if (Keyboard.GetState().IsKeyDown(Keys.LeftShift)) deltaDistance *= 2;
distance += deltaDistance + preDeltaDistance;
//momentum
preDeltaDistance += deltaDistance;
preDeltaDistance /= 20.0f;
if (preDeltaDistance < 0.1f) preDeltaDistance = 0.0f;
//Translate (pan)
float deltaRight = 0.0f;
float deltaDown = 0.0f;
//Mouse Middle Click
if (Mouse.GetState().MiddleButton == ButtonState.Pressed)
{
deltaDown = (Mouse.GetState().Y - preMouse.Y) / 10f;
deltaRight = (Mouse.GetState().X - preMouse.X) / 10f;
}
//Keyboard: AWSD
if( Keyboard.GetState().IsKeyDown(Keys.A)) deltaRight += 1f;
if (Keyboard.GetState().IsKeyDown(Keys.D)) deltaRight -= 1f;
if (Keyboard.GetState().IsKeyDown(Keys.W)) deltaDown += 1f;
if (Keyboard.GetState().IsKeyDown(Keys.S)) deltaDown -= 1f;
//Make translation faster with shift/control pressed
if (Keyboard.GetState().IsKeyDown(Keys.LeftShift))
{
deltaRight *= 2.0f;
deltaDown *= 2.0f;
}
if (Keyboard.GetState().IsKeyDown(Keys.LeftControl))
{
deltaRight *= 2.0f;
deltaDown *= 2.0f;
}
deltaRight += preDeltaRight;
deltaDown += preDeltaDown;
Vector3 ViewRight = Vector3.Transform(Vector3.UnitX,
Matrix.CreateRotationX(cameraAngleX) * Matrix.CreateRotationY(cameraAngleY));
Vector3 ViewUp = Vector3.Transform(Vector3.UnitY,
Matrix.CreateRotationX(cameraAngleX) * Matrix.CreateRotationY(cameraAngleY));
cameraTarget -= ViewRight * deltaRight;
cameraTarget += ViewUp * deltaDown;
//momentum for deltaRight/Down
preDeltaDown = deltaDown / 20.0f;
preDeltaRight = deltaRight / 20.0f;
preWorldViewProjection = worldViewProjection; //previous view projection -- for blur
//keep previous mouse/keyboard states
preMouse = Mouse.GetState();
preKeyboard = Keyboard.GetState();
cameraPosition = Vector3.Transform(new Vector3(0, 0, distance),
Matrix.CreateRotationX(cameraAngleX) * Matrix.CreateRotationY(cameraAngleY) * Matrix.CreateTranslation(cameraTarget));
view = Matrix.CreateLookAt(
cameraPosition,
cameraTarget,
Vector3.Transform(Vector3.UnitY, Matrix.CreateRotationX(cameraAngleX) * Matrix.CreateRotationY(cameraAngleY)));
lightPosition = Vector3.Transform(
new Vector3(0, 0, lightDistance),
Matrix.CreateRotationX(lightAngleX) * Matrix.CreateRotationY(lightAngleY));
lightView = Matrix.CreateLookAt(lightPosition, Vector3.Zero, Vector3.UnitY);
worldViewProjection = world * view * projection;
base.Update(gameTime);
}
float4x4 World;
float4x4 View;
float4x4 Projection;
float4x4 WorldInverseTranspose;
//Light options
float4 AmbientColor;
float4 DiffuseColor;
float4 SpecularColor;
float Shininess;
float SpecularIntensity;
float DiffuseIntensity;
float3 LightPosition;
float3 CameraPosition;
struct VertexShaderInput {
float4 Position: POSITION;
float4 Normal: NORMAL;
float2 TexCoord : TEXCOORD0;
};
struct VertexShaderOutput {
float4 Position: POSITION;
float4 Color: COLOR;
float4 Normal : TEXCOORD0;
float4 WorldPosition : TEXCOORD1;
float2 TexCoord : TEXCOORD2;
};
texture UVTexture;
sampler UVSampler = sampler_state
{
Texture = ;
MinFilter = LINEAR;
MagFilter = LINEAR;
MipFilter = LINEAR;
AddressU = CLAMP;
AddressV = CLAMP;
};
//Vertex shader
VertexShaderOutput PhongVertexShaderFunction(VertexShaderInput input) {
VertexShaderOutput output;
output.WorldPosition = mul(input.Position, World);
output.Position = mul(mul(output.WorldPosition, View), Projection);
output.Normal = mul(input.Normal, WorldInverseTranspose);
output.Color = 0;
output.TexCoord = input.TexCoord;
return output;
}
//Pixel Shader
float4 PhongPixelShaderFunction(VertexShaderOutput input) : COLOR{
float3 N = normalize(input.Normal.xyz);
float3 V = normalize(CameraPosition - input.WorldPosition.xyz);
float3 L = normalize(LightPosition);
float3 R = reflect(-L, N);
float facing = dot(N, L) > 0 ? 1 : 0;
float4 diffuse = DiffuseIntensity * DiffuseColor * max(0, dot(N, L));
float4 specular = SpecularIntensity * SpecularColor*max(0, dot(N, L))*facing;
//do 50% color and 50% texture
return lerp(tex2D(UVSampler, input.TexCoord), (AmbientColor + diffuse*DiffuseColor + specular*SpecularColor), 0.5);
}
technique Phong
{
pass Pass1
{
VertexShader = compile vs_4_0 PhongVertexShaderFunction();
PixelShader = compile ps_4_0 PhongPixelShaderFunction();
}
}
Here, implement a depth map shader, which will be used by this post-processing motion blur effect.
The velocity calculation part in the Pixel Shader impacts the quality of the blur effect produced. GPU Gems divides the difference of (lastPosition - newPosition) by 2, but since the calculations are based on screen coordinates, it seems that those values should vary depending on the sizes of models in the scene, the distance from the camera, and also on the axes on which the movement is actually done. From my experiments, I’ve noticed that different factors work better for “zoom” and “pan or rotation”, so i decided to use a different factor for each.
float4x4 WorldViewProjection;
float4x4 InvWorldViewProjection;
float4x4 preWorldViewProjection;
float4x4 preInvWorldViewProjection;
float4x4 WorldInverseTranspose;
float NumSamples = 8;
float isZoom = 0.0f;
texture DepthMap;
sampler DepthMapSampler = sampler_state
{
Texture = ;
MinFilter = POINT;
MagFilter = POINT;
MipFilter = POINT;
AddressU = CLAMP;
AddressV = CLAMP;
AddressW = CLAMP;
};
texture SceneTexture;
sampler SceneSampler = sampler_state
{
Texture = ;
MinFilter = POINT;
MagFilter = POINT;
MipFilter = POINT;
AddressU = CLAMP;
AddressV = CLAMP;
AddressW = CLAMP;
};
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float4 Position2D : TEXCOORD0;
};
VertexShaderOutput DepthMapVertexShader(VertexShaderInput input)
{
VertexShaderOutput output;
output.Position = mul(input.Position, WorldViewProjection);
output.Position2D = output.Position;
return output;
}
float4 DepthMapPixelShader(VertexShaderOutput input) : COLOR0
{
float4 projTexCoord = input.Position.z / input.Position.w;
projTexCoord.xy = 0.5f * projTexCoord.xy + float2(0.5f, 0.5f);
projTexCoord.y = 1.0f - projTexCoord.y;
float depth = 1.0f - projTexCoord;
float4 color = (depth>0) ? depth : 0;
return float4(color.r,0,0,1);
}
struct ppVertexShaderOutput {
float2 UV0 : TEXCOORD0;
};
ppVertexShaderOutput vsScreenUV(float4 inPos : POSITION, float2 inTex : TEXCOORD0) {
ppVertexShaderOutput output;
output.UV0 = inTex;
return output;
}
float4 BlurredScenePixelShader(float4 position : SV_Position, float4 colorIn : COLOR0, float2 texCoordIn : TEXCOORD0) : COLOR0{
float2 texCoord = texCoordIn;
// Get the depth buffer value at this pixel.
float zOverW = tex2D(DepthMapSampler, texCoord).r;
// H is the viewport position at this pixel in the range -1 to 1.
float4 H = float4(texCoord.x * 2 - 1, (1 - texCoord.y) * 2 - 1, (1-zOverW)*2-1, 1);
// Transform by the view-projection inverse.
float4 D = mul(H, InvWorldViewProjection);
// Divide by w to get the world position.
float4 worldPos = D / D.w;
// Current viewport position
float4 currentPos = H;
// Use the world position, and transform by the previous view-
// projection matrix.
float4 previousPos = mul(worldPos, preWorldViewProjection);
// Convert to nonhomogeneous points [-1,1] by dividing by w.
previousPos /= previousPos.w;
// Use this frame's position and last frame's to compute the pixel
// velocity.
float2 velocity = ((currentPos.xy - previousPos.xy))/2.0f;
velocity.y *= -1; //flip y (NDC y would be opposite)
//Need to divide by a number, that also depends on the scene, to get samples around the texture
if ( isZoom > 0 )
velocity.xy /= (2.0f * NumSamples * NumSamples); //for zoom
else
velocity.xy /= (NumSamples * 1500.0f); //for rotation, pan
// Get the initial color at this pixel.
float3 color = tex2D(SceneSampler, texCoord.xy).rgb;
texCoord += velocity;
for (int i = 1; i < NumSamples; ++i, texCoord += velocity)
{
// Sample the color buffer along the velocity vector.
float3 currentColor = tex2D(SceneSampler, texCoord.xy).rgb;
// Add the current color to our color sum.
color += currentColor;
}
// Average all of the samples to get the final blur color.
float3 finalColor = color.rgb / (NumSamples);
return saturate(float4(finalColor, 1.0f));
}
technique DepthMapTechnique
{
pass Pass1
{
VertexShader = compile vs_4_0 DepthMapVertexShader();
PixelShader = compile ps_4_0 DepthMapPixelShader();
}
}
technique BlurredScenePostProcessTechnique
{
pass Pass1
{
PixelShader = compile ps_4_0 BlurredScenePixelShader();
}
}
private void DrawLitScene()
{
//Use the light shader to render the phong-shaded scene for use as a texture
lightShader.CurrentTechnique = lightShader.Techniques[0];
RasterizerState s = new RasterizerState();
s.CullMode = CullMode.None;
GraphicsDevice.RasterizerState = s;
DepthStencilState ss = new DepthStencilState();
//ss.DepthBufferFunction = CompareFunction.LessEqual;
GraphicsDevice.DepthStencilState = ss;
for(int i = 0; i < models.Length; i++)
{
Model model = models[i];
foreach (EffectPass pass in lightShader.CurrentTechnique.Passes)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (ModelMeshPart part in mesh.MeshParts)
{
lightShader.Parameters["World"].SetValue(modelTransform[i] * mesh.ParentBone.Transform);
lightShader.Parameters["View"].SetValue(view);
lightShader.Parameters["Projection"].SetValue(projection);
lightShader.Parameters["AmbientColor"].SetValue(ambient);
lightShader.Parameters["DiffuseColor"].SetValue(diffuseColor);
lightShader.Parameters["DiffuseIntensity"].SetValue(diffuseIntensity);
lightShader.Parameters["SpecularColor"].SetValue(specularColor);
lightShader.Parameters["SpecularIntensity"].SetValue(specularIntensity);
lightShader.Parameters["LightPosition"].SetValue(lightPosition);
lightShader.Parameters["CameraPosition"].SetValue(cameraPosition);
lightShader.Parameters["Shininess"].SetValue(shininess);
//Apply texture from model, if included
if (mesh.Effects.Count > 0)
{
Texture2D texture = (Texture2D)((BasicEffect)mesh.Effects[0]).Texture;
lightShader.Parameters["UVTexture"].SetValue(texture);
}
Matrix worldInverseTranspose = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform));
lightShader.Parameters["WorldInverseTranspose"].SetValue(worldInverseTranspose);
pass.Apply();
// set buffers and draw mesh model
GraphicsDevice.SetVertexBuffer(part.VertexBuffer);
GraphicsDevice.Indices = part.IndexBuffer;
GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
part.VertexOffset,
part.StartIndex,
part.PrimitiveCount
);
}
}
}
}
}
private void DrawDepthMap()
{
//Draw depth map in texture for use with blur effect
blurShader.CurrentTechnique = blurShader.Techniques[0]; //depthMap technique
for (int i = 0; i < models.Length; i++)
{
Model model = models[i];
foreach (EffectPass pass in blurShader.CurrentTechnique.Passes)
{
foreach (ModelMesh mesh in model.Meshes)
{
foreach (ModelMeshPart part in mesh.MeshParts)
{
blurShader.Parameters["WorldViewProjection"].SetValue( mesh.ParentBone.Transform * view * projection );
Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform));
blurShader.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
pass.Apply();
GraphicsDevice.SetVertexBuffer(part.VertexBuffer);
GraphicsDevice.Indices = part.IndexBuffer;
GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList,
part.VertexOffset,
part.StartIndex,
part.PrimitiveCount);
}
}
}
}
}
private void DrawBlurredScene()
{
//Draw the blurred scene (combines lit scene with information from depth map + previous World-View-Projection matrix)
blurShader.CurrentTechnique = blurShader.Techniques[1]; //blur post-processing blurShader
blurShader.Parameters["WorldViewProjection"].SetValue(worldViewProjection);
blurShader.Parameters["InvWorldViewProjection"].SetValue(Matrix.Invert(worldViewProjection));
blurShader.Parameters["preWorldViewProjection"].SetValue(preWorldViewProjection);
blurShader.Parameters["preInvWorldViewProjection"].SetValue(Matrix.Invert(preWorldViewProjection));
Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(Matrix.Identity));
blurShader.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
blurShader.Parameters["DepthMap"].SetValue(this.depthMap);
blurShader.Parameters["SceneTexture"].SetValue(this.litScene);
blurShader.CurrentTechnique.Passes[0].Apply();
using (SpriteBatch spriteBatch = new SpriteBatch(GraphicsDevice))
{
spriteBatch.Begin(SpriteSortMode.Texture, BlendState.Opaque, null, null, null, blurShader);
spriteBatch.Draw(litScene, new Rectangle(0, 0, Window.ClientBounds.Width, Window.ClientBounds.Height), Color.White);
spriteBatch.End();
}
}
private void DrawText()
{
//Draw some help text
string[] text = new string[]
{
"CAMERA CONTROLS:",
"Translation: W,A,S,D; Mouse Middle click",
"Rotation: T,F,G,H; Mouse Left click",
"Zoom in: X; Zoom out: X; Mouse Right click",
"Pressing Ctrl/Shift increases movement speed by 2x. (4x with both)",
"To enable/disable blur effect, press B [Currently " + (this.drawBlurred ? "Enabled" : "Disabled") + "]"
};
Vector2 pos = Vector2.Zero;
float lineHeight = font.MeasureString(text[0]).Y;
using (SpriteBatch spriteBatch = new SpriteBatch(GraphicsDevice))
{
spriteBatch.Begin(0, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, null);
for(int i = 0; i < text.Length; i++)
{
spriteBatch.DrawString(font, text[i], pos, Color.White);
pos.Y += lineHeight;
}
spriteBatch.End();
}
}
protected override void Draw(GameTime gameTime)
{
if (this.drawBlurred)
{
//Set render target for lit scene
GraphicsDevice.SetRenderTarget(litSceneRenderTarget);
GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1.0f, 0);
//Render Lit scene
GraphicsDevice.DepthStencilState = new DepthStencilState();
DrawLitScene();
litScene = (Texture2D)litSceneRenderTarget;
//Set render target for shadow map
GraphicsDevice.SetRenderTarget(depthMapRenderTarget);
GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
//Render Shadow map
GraphicsDevice.DepthStencilState = new DepthStencilState();
DrawDepthMap();
depthMap = (Texture2D)depthMapRenderTarget;
//Set render target to screen
GraphicsDevice.SetRenderTarget(null);
//Clear the render target
GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.White, 1.0f, 0);
//Draw Blurred Scene
DrawBlurredScene();
}
else //Draw lit scene, no blurring
{
//Set render target for lit scene
GraphicsDevice.SetRenderTarget(null);
GraphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1.0f, 0);
//Render Lit scene
GraphicsDevice.BlendState = BlendState.Opaque;
GraphicsDevice.DepthStencilState = new DepthStencilState();
DrawLitScene();
}
DrawText();
base.Draw(gameTime);
}
}
You can download the code, as well as a compiled version from github at (tested on Windows 10): https://github.com/mhadjimichael/gpugems-motionblur