Once I had the core mechanic of Metric Racer down, I tried a number of ways to increase perceived speed to make it seem that the player was moving faster than they actually were in the physics engine.
Initially I tried increasing the camera field of view as the racer’s speed increased, this worked (and is still in the game) but I was looking for more.
To cut down on resource usage, I tried faking a camera motion blur by fading in the Blurred Scene Render target from the previous scene to sort of fake a camera motion blur. Increasing the amount of added blur as the speed increased. This sort of worked but gave some artifacts if there was a large enough speed drop or increase.
So I decided to tackle a camera motion blur shader, which in the end turned out to be a rather straight forward shader once I had the method down.
Choosing a Type
There’s 2 ways to do motion blur, Camera Motion blur, where the scene is based on the amount of camera movement, and Per-Object blur, which blurs each object based on it’s individual velocity.
For Metric Racer I’m only using Camera Motion blur since the effect wanted is to give the feeling of increased speed without having to actually increase that speed.
The Concept
In the real world, the amount of an object blurring is due to a camera’s shutter speed setting. The camera sensor (or film in some cases) is taking in light while the object is moving. If the object is moving fast enough that the shutter is still open while the object moves, then the camera sensor (or film) picks up light from that object as it moves, blurring it.
In Graphics programming, we take snap shots of the world generally 30-60 times per second (at least that rate is the goal). This means that an object is rendered at their position “statically” each frame. If we just took one frame we likely couldn’t tell which objects are moving and which aren’t. If we keep the physical camera analogy, Rendering a frame is like taking a picture with an instantaneous shutter speed.
But since we have data of our objects that carry on from frame to frame, What we can do to give the feeling of movement is to blur the pixel colour values between the current and previous frames. It’s finding which pixels to blur with each other is the trick.
The Algorithm
The method of actually implementing only requires a few steps and is rather straight forward. The method goes:
Find The Current Position
For each pixel, we find the 3D world space position from the depth and UV’s of that pixel and from using the Camera’s InverseViewProjection matrix for the current frame.
float depth = GetDepth(texCoord); // Get the World Position float4 worldPos = 1.0f; // Convert the texCoord UV values worldPos.x = (texCoord.x * 2.0f - 1.0f); worldPos.y = -(texCoord.y * 2.0f - 1.0f); worldPos.z = depth; //Transform the Position into World Space worldPos = mul(worldPos, InverseViewProjection); worldPos /= worldPos.w;
Get the Previous UV Coordinates
Now that we have the current Position in World Space for this pixel, We can then take the Camera’s ViewProjection matrix from the previous frame to get the the UV coordinates of that world position from the last frame. We now have two UV coordinates.
float4 prevUV = mul(float4(worldPos, 1.0f), PrevViewProjection); // Now convert the UVpos prevUV.xy = float2(0.5f, 0.5f) + float2(0.5f, -0.5f) * prevUV.xy / prevUV.w; // return the UV pos with the depth value at that location float3 prevUVDepth = float3(prevUV.xy, prevUV.z / prevUV.w);
Blend and Blur
We can then find the vector (blendVector) in screen space between the current and previous world positions UV coordinates. We then blur between these two positions using difference values and blurring methods to get the result we want.
// Get the Blend Vector float2 blendVector = (texCoord-prevUVDepth.xy) * 1.5f; // Get the scene's colour at the current frames UV coord float4 currentColor = tex2D(SceneTextureSampler, texCoord); // This will be the blurred colour result float4 BlurredColor = currentColor; for (int i = 0; i < SAMPLE_COUNT; i++) { // Get the new UV coordinates somewhere on the blendVector float2 txcrd = texCoord - length(RAND_SAMPLES[i].xy) * blendVector; BlurredColor += tex2D(SceneTextureSampler, txcrd); }
The Initial Result
After an initial try, this was the result in the Vertices Test Bed.
Fine Tuning
This generally gives a good look. although there are some caveats and short falls that can cause artifacts and issues.
Off Screen Positions
If one of the World Positions is offscreen between frames, then the effect will only be able to return a UV value of 0 or 1. If the UV coordinate is far enough off screen, What this creates is a banding along the edges.
I didn’t initially find this out. The base method works great in our test bed where the speed and movement is similar to a person, but when I ran Metric Racer with the effect on, the speed of the player is so high that a third of the screen was giving this off-screen UV artifact.
My first fix was to find the intersection point with the screen along the blend vector and ‘reset’ the previous frames UV at this intersection point, but as this would add a few lines to our code, I found by simply knocking down the blend vector by 90%, this would remove the banding issues without much change in quality of the output.
Adding the following line during the blur loop solves the issue:
// Knock down the blend vector to 10% if it's off screen if (txcrd.y <= 0 || txcrd.x <= 0 || txcrd.y >= 1 || txcrd.x >= 1) txcrd = texCoord - length(RAND_SAMPLES[i].xy) * blendVector * 0.1f ;
Camera Focused Items
Since this is a Camera Motion blur, and not per-object blur, the scene blurs everything on the screen by it’s position from the last frame. This means that if the camera is following an object (like the player in Metric) then it will still blur this, which gives a really strange look.
The fix for this is masking out which objects on screen shouldn’t be blurred. By doing a simple ‘mask check’ for a specific value from the mask render target (the Black and Green View), I get the a value that can factor the blur amount.
Adding a simple lerp during the blur loop solves this issue:
BlurredColor += lerp(tex2D(SceneTextureSampler, txcrd), currentColor, GetMask(txcrd));
The End Result
If you want to see a contrast, take a look at the initial video here.
Cross Platform Magic
The entire point of using MonoGame is the ease of use with it for creating cross platform games. But what I can never get over is how 9 times out of 10, when I implement a feature, or especially when I write a new shader, when I take it over to another platform, it just works. I wrote this partially on my macbook, then polished on the windows PC, and then brought it back over and it just worked beautifully.
Final Thoughts
I was able to get this working in an afternoon, there certainly are some optimizations and more fine tuning, but even with extensive blurring along the blend vector, this gives a really great result with a very little footprint.
Reblogged this on rtroe and commented:
Entry Number 2 in the MonoGame Shader Series I’m writing over at Virtex Edge Designs site is on Camera Motion Blur. The technique is pretty straight forward but adds a great amount to any scene. Check it out below and don’t forget to give Virtex and my self a follow to keep up with the shader series and our upcoming releases.