Contents |
Motion blur is a form of temporal anti-aliasing. It occurs in movies because of the finite exposure of the camera when moving objects are recorded.
Motion blur effects are commonly a combination of two techniques called velocity vector field and geometry stretching.
To achieve a per-pixel blur effect a vector field that expresses the motion of each vertex of any moving object [Shimizu] is stored (see [Green] for an implementation of a vector field). Each vector in this field can be determined by the direction and speed of the motion and is stored in homogenous post-projection space (HPPS) as shown in Equation 27.
The previous HPPS value in pP is subtracted from the current position value and the value range is then scaled from -2..2 to -1..1. This vector field is stored in a render target and is used in a following pass to blur the color image.
To achieve a per-vertex blur effect a technique called geometry stretching can be utilized [Wloka]. If the motion vector points in a similar direction as the normal vector -both in view or world space- the current position value is chosen to transform the vertex, otherwise the previous position value is chosen.
<center>
<center>Equation 28The geometry is stretched if the vertex faces away from the direction of the motion and therefore the vertex is transformed to its previous position, whereas other vertices might be transformed to the current position value.
Implementing geometry stretching is done in the vertex shader and rather straightforward:
// transform previous and current pos float4 P = mul(WorldView, in.Pos); float4 Pprev = mul(prevWorldView, in.prevPos); // transform normal float3 N = mul(WorldView, in.Normal); // calculate eye space motion vector float3 motionVector = P.xyz - Pprev.xyz; // calculate clip space motion vector P = mul(WorldViewProj, in.Pos); Pprev = mul(prevWorldViewProj, in.prevPos); // choose previous or current position based // on dot product between motion vector and normal float flag = dot(motionVector, N) > 0; float4 Pstretch = flag ? P : Pprev; out.hpos = Pstretch;
To implement a velocity vector field, the view and world matrices for all objects from the previous frame need to be stored. Based on those matrices and the current view and world matrices of all objects, velocity data can be written to a render target (that might be part of a multiple render -target) following equation 27:
// do divide by W -> NDC coordinates P.xyz = P.xyz / P.w; Pprev.xyz = Pprev.xyz / Pprev.w; Pstretch.xyz = Pstretch.xyz / Pstretch.w; // calculate window space velocity out.velocity = (P.xyz - Pprev.xyz);
An alternative approach would reconstruct the previous HPPS values from the depth buffer as a separate pass. This can be done by re-transforming by the previous view matrix to calculate the pixel velocity. Because it re-constructs with the help of the view matrices, this approach does not account for frame-by-frame changes in world space.
A shortcoming of any velocity field approach is that the pixel velocity is calculated per-vertex and interpolated across primitives. Since the near clip plane can lie in front of the zero plane, position values are transformed into projection space per vertex and the perspective divide is done per pixel to avoid divide by zero errors. The resulting current and previous position values are subtracted from each other, adjusted into the correct range as shown in equation 27 and usually scaled by a game-specific scalar.
The blur value is taken from a usually down-sampled, blurred quarter-size render target. To reduce banding artefacts while fetching this render target as a texture, the texture coordinates can be offset for each sample by an amount of jitter.
Motion blur is then applied to the main frame buffer by interpolating between the value coming from the blurred render target and the original full-resolution render target based on the length (speed) of the velocity vectors at each sample point. To reduce bleeding between regions of differing velocity, each sample's color contribution is scaled based on the length of the velocity vector.
float2 velocity = tex2D(VelocityMapSampler, in.tex.xy); static const int numSamples = 8; float2 velocityStep = velocity / numSamples; float speed = length(velocity); float3 FullscreenSample = tex2D(FullMapSampler, IN.tex.xy); // Look up into our jitter texture using this 'magic' formula static const half2 JitterOffset = { 58.164f, 47.13f }; const float2 jitterLookup = IN.tex.xy * JitterOffset + (FullscreenSample.rg * 8.0f); JitterScalar = tex2D(JitterMapSampler, jitterLookup) - 0.5f; IN.tex.xy += velocityStep * JitterScalar; for (int i = 0; i < numSamples / 2; ++i) { float2 currentVelocityStep = velocityStep * (i + 1); sampleAverage += float4(tex2D(QuarterMapSampler, IN.tex.xy + currentVelocityStep), 1.0f); sampleAverage += float4(tex2D(QuarterMapSampler, IN.tex.xy - currentVelocityStep), 1.0f); } float3 outColor = sampleAverage.rgb / sampleAverage.w; float2 normScreenPos = IN.tex.xy; float2 vecToCenter = normScreenPos - 0.5f; float distToCenter = pow(length(vecToCenter) * 2.25f, 0.8f); float3 desat = dot(outColor.rgb,float3(0.3f,0.59f,0.11f)); outColor = lerp(outColor, desat, distToCenter * blurEffectDesatScale); float3 tinted = outColor.rgb * blurEffectTintColor; outColor.rgb = lerp(outColor.rgb, tinted.rgb, distToCenter * blurEffectTintFade); return float4(outColor.rgb, velMag);
Storing this velocity vector additionally in the alpha channel of the render target makes it available to following post-processing pipeline passes.