Hexagonal Bokeh Blur Revisited – Part 4: Rhombi Overlap

This post is from a multi-part series titled “Hexagonal Bokeh Blur Revisited”. Want to jump directly to the index? If so, click here.

Another common artifact is the Y-shaped pattern of overlapping rhombi:

YShape.pngY-shaped Artifact

From the first post in this series, you might remember our blur function:

float4 BlurTexture(sampler2D tex, float2 uv, float2 direction)
{
    float4 finalColor = 0.0f;
    float blurAmount = 0.0f;
 
    // This offset is important. Will explain later. ;)
    uv += direction * 0.5f;
 
    for (int i = 0; i < NUM_SAMPLES; ++i)
    {
        float4 color = tex2D(tex, uv + direction * i);
        color *= color.a;
        blurAmount += color.a; 
        finalColor += color;
    }
 
    return (finalColor / blurAmount);
}

The half sample offset highlighted in bold shows how to prevent this issue.

BokehY.pngRhombi Overlap (Left) vs Proper Alignment (Right)

Steve Hill reminded me that this was actually mentioned in the notes on slide 15:

We also apply a half sample offset to stop overlapping rhombi. Otherwise you’ll end up with a double brightening artifact in an upside Y shape.

As you can see, it’s easily solvable! 😀

Hexagonal Bokeh Blur Revisited – Part 3: Additional Features: Rotation

This post is from a multi-part series titled “Hexagonal Bokeh Blur Revisited”. Want to jump directly to the index? If so, click here.

So far, we’ve shown how to build a separate hexagonal blur in two passes. While the shape is interesting in its basic form, one can definitely change it.

For example: rotation!

GRWLRotated Hexagonal Bokeh Depth-of-Field in Ghost Recon Wildlands

Alternatively, works really nicely with a ton of them!

RotatedBokeh.png
Separable Hexagonal Bokeh Blur – Demo On Github

It’s Actually Quite Simple…

While this might sound obvious to many of you out there, I’ve had 2 people mention on separate occasions that they had issues achieving this. Might be with the way they approached the hexagonal blur, but with our separable approach it’s actually quite simple.

Just offset your angles and let the trigonometry do its magic. 

float2 blurDir = coc * invViewDims * float2(cos(angle + PI/2), sin(angle + PI/2));

Hexagonal Bokeh Blur Revisited – Part 2: Improved 2-pass Version

This post is from a multi-part series titled “Hexagonal Bokeh Blur Revisited”. Want to jump directly to the index? If so, click here.

As seen previously, we can achieve this blur in a pretty straightforward fashion in three passes. The code below demonstrates an improvement over such approach, by achieving the blur in two passes. Since it builds on the previous post, make sure to read it beforehand.

If this is obvious to you, I invite you to skip to the next part.

Step 1 – Combined Vertical & Diagonal Blur

We have MRTs, so let’s combine both blurs in the same pass.

Combined4.png

struct PSOUTPUT
{
    float4 vertical : COLOR0;
    float4 diagonal : COLOR1;
};

// Get the local CoC to determine the radius of the blur.
float coc = tex2D(sceneTexture, uv).a; 

// CoC-weighted vertical blur.
float2 blurDir = coc * invViewDims * float2(cos(PI/2), sin(PI/2));
float4 color = BlurTexture(sceneTexture, uv, blurDir) * coc; 

// CoC-weighted diagonal blur.
float2 blurDir2 = CoC * invViewDims * float2(cos(-PI/6), sin(-PI/6));
float4 color2 = BlurTexture(sceneTexture, uv, blurDir2) * coc;

// Output to MRT
PSOUTPUT output;
output.vertical = float4(color.rgb, coc);
output.diagonal = float4(color2.rgb + output.vertical.xyz, coc);

Much simpler! Also means we don’t have to read a temporary (vertical) buffer unlike in the previous 3-pass approach, since we’re doing this all at once.

Step 2 – Rhomboid Blur

Combined5.png

The final step is the rhomboid blur. This is similar to the 3-pass approach. Again, this is done in two parts: via a 30 degrees (-PI/6) blur, as well as its reflection at 150 degrees (-5PI/6).

// Get the center to determine the radius of the blur
float coc = tex2D(verticalBlurTexture, uv).a;
float coc2 = tex2D(diagonalBlurTexture, uv).a;

// Sample the vertical blur (1st MRT) texture with this new blur direction
float2 blurDir = coc * invViewDims * float2(cos(-PI/6), sin(-PI/6));
float4 color = BlurTexture(verticalBlurTexture, uv, blurDir) * coc;

// Sample the diagonal blur (2nd MRT) texture with this new blur direction
float2 blurDir2 = coc2 * invViewDims * float2(cos(-5*PI/6), sin(-5*PI/6));
float4 color2 = BlurTexture(diagonalBlurTexture, uv, blurDir2) * coc2;
 
float3 output = (color.rgb + color2.rgb) * 0.5f;

Well That Was Kind of Obvious…

Yup! Just making sure. Details provided for posterity, and I’ll also be building on this part and the previous for the upcoming sections.

Again, a code sample is provided here. You should be able to toggle between both versions and see… that there is no difference.

Hexagonal Bokeh Blur Revisited – Part 1: Basic 3-pass Version

This post is from a multi-part series titled “Hexagonal Bokeh Blur Revisited“. Want to jump directly to the index? If so, click here.

The code below demonstrates the most straightforward way to achieve this blur. It is done in 3 passes.

Animation

Step 0 – Blur Function

First, let’s define our blur function. This will be reused along the way.

float4 BlurTexture(sampler2D tex, float2 uv, float2 direction)
{
    float4 finalColor = 0.0f;
    float blurAmount = 0.0f;
 
    // This offset is important. Will explain later. ;)
    uv += direction * 0.5f;
 
    for (int i = 0; i < NUM_SAMPLES; ++i)
    {
        float4 color = tex2D(tex, uv + direction * i);
        color *= color.a;
        blurAmount += color.a; 
        finalColor += color;
    }
 
    return (finalColor / blurAmount);
}

Step 1 – Vertical Blur

First, we blur vertically.

Combined1

// Get the local CoC to determine the radius of the blur.
float coc = tex2D(sceneTexture, uv).a; 

// CoC-weighted vertical blur.
float2 blurDirection = coc * invViewDims * float2(cos(PI/2), sin(PI/2));
float3 color = BlurTexture(sceneTexture, uv, blurDirection) * coc;

// Done!
return float4(color, coc);

Step 2 – Diagonal Blur

Second we blur diagonally.

This stage is similar to Stage 1, but now with a 30 degree (PI/6) angle. We also combine the diagonal blur with the vertical blur.

Combined2.png

// CoC-weighted diagonal blur
float2 blurDir = coc * invViewDims * float2(cos(-PI/6), sin(-PI/6));
float4 color = BlurTexture(verticalBlurTexture, uv, blurDir) * coc;

// Combine with the vertical blur 
// We don't need to divide by 2 here, because there is no overlap 
return float4(color.xyz + tex2D(verticalBlurTexture, uv).rgb, coc);

Which gives:

4_SceneBottomRight

Step 3 – Rhomboid Blur

The final step is the rhomboid blur.

This is done in two parts: via a 30 degrees (PI/6) blur, as well as its reflection at 150 degrees (5PI/6).

Combined3.png

// Get the center to determine the radius of the blur
float coc = tex2D(verticalBlurTexture, uv).a;
float coc2 = tex2D(diagonalBlurTexture, uv).a;

// Sample the vertical blur (1st MRT) texture with this new blur direction
float2 blurDir = coc * invViewDims * float2(cos(-PI/6), sin(-PI/6));
float4 color = BlurTexture(verticalBlurTexture, uv, blurDir) * coc;

// Sample the diagonal blur (2nd MRT) texture with this new blur direction
float2 blurDir2 = coc2 * invViewDims * float2(cos(-5*PI/6), sin(-5*PI/6));
float4 color2 = BlurTexture(diagonalBlurTexture, uv, blurDir2) * coc2;

// And we're done!
float3 output = (color.rgb + color2.rgb) * 0.5f;

Putting It All Together

Animation

As you can see, the code listed previously is pretty straightforward and should be a good base for you to achieve this blur. Additionally a code sample is provided here.

We can do better. Let’s do it in 2 passes!

Hexagonal Bokeh Blur Revisited

Hello Bokeh, My Old Friend

I’ve come to talk with you again…

It’s been a while. The last time we spoke to each other was back at SIGGRAPH 2011 in the Advances in Real-Time Rendering course with John White.

SIG2011.png

NFS.png
Separable Hexagonal Bokeh Depth-of-Field in Need For Speed: The Run

You’ve Been Around

Back then, we didn’t give out the code on how to achieve this effect. It turns out many developers out there were still able to realize it, solely based on John’s slides and notes!

WD1.png
Watch Dogs – Ubisoft

GRWL.png
Ghost Recon Wildlands – Ubisoft

Tom Clancy’s The Division – Ubisoft

MKXL.jpg
Mortal Kombat X – Netherrealm (WB Games)

SE2.png
Sniper Elite 2 – Rebellion


NBA 2K 2014

nba_2k17_22
NBA 2K 2017

gow.png
Gears of War: Ultimate Edition

Gjoell.pngMikkel Gjoel’s – ShaderToy

evanwallace.png
Evan Wallace’s – WebGL Lens Filter

We Meet Again?

While the technique presented six years ago has been showcased in a myriad of games and can be easily implemented in a straightforward way, it turns out some of the implementations out there have unfortunate visual artifacts. 😦

Artifacts.png

Luckily these artifacts can be easily solved! 😀

Back then, for the sake of time John left out information regarding circle-of-confusion management, as well as other details. If not handled correctly, this omission could lead to some unwanted artifacts, like in the images above. Again it’s been six years since the presentation, so I feel it’s time we set everything straight and clear these artifacts out.

The goal behind this post is to provide an “artifact-free” implementation, or rather a code companion complementary to the SIGGRAPH 2011 presentation. The code is hosted on Github.

I’m super busy, I want to blog more and I want to make this manageable, so this post is split in multiple parts. Once the whole series is done, I might collapse all the parts into something more concise.

In the meantime, thanks for stopping by! 🙂

Index

Acknowledgements

Thanks to John White for coming up with the original idea of “scatter-gather” separable hexagonal bokeh depth of field by rhomboid decomposition. I really miss the days when we used to work together, back when I was on Battlefield 3 and he was on NFS: The Run. Crazy-but-good times with lots of good exchanges. We shared a lot, and I sure learned a lot. Thanks John! 🙂

References

WHITE, John, and BARRÉ-BRISEBOIS, Colin. More Performance! Five Rendering Ideas From Battlefield 3 and Need For Speed: The Run, Advances in Real-Time Rendering in Games, SIGGRAPH 2011. Available Online.