DEV Community

Cover image for Unpacking the Math: Building a Custom Miniature-Style DoF in UE with HLSL
taku25
taku25

Posted on

Unpacking the Math: Building a Custom Miniature-Style DoF in UE with HLSL

Lately, I've been deep into developing Neovim plugins for Unreal Engine, which ironically involves more configuration than actual coding. To switch things up, I decided to dive back into the engine itself—not by writing C++, but by playing with the Material Editor. My goal: to create a custom Depth of Field (DoF) post-process effect from scratch.

At first, I tried using a Cine Camera with a telephoto lens and a low F-stop. While it produced a beautiful, cinematic bokeh, the camera had to be too far from the player character, making it impractical for gameplay. That's when I decided to implement it as a post-process effect using HLSL.

In this article, I'll walk you through the process, from modifying the core formula to implementing it in HLSL and even experimenting with custom bokeh shapes.


Making the Circle of Confusion (CoC) Formula More Intuitive

The core of any DoF effect is the Circle of Confusion (CoC). It's a value that determines how large and blurry an out-of-focus point on the screen should be.

The standard, physically-based formula for CoC is:

CoC=f2NPSS(Pf) CoC = \frac{f^2}{N} \frac{|P - S|}{S(P - f)}
  • ff : focal length of the lens
  • NN : F-number
  • PP v: distance to the focus plane
  • SS : distance to the subject

While this formula is accurate, I find it a bit unintuitive for real-time tweaking, especially with the F-number ( NN ) in the denominator. So, I opted for a modified formula that uses the aperture size directly:

CoC=AfPSS CoC = \frac{A \cdot f \cdot |P - S|}{S}
  • AA : Aperture size
  • Other variables are the same.

This version makes more sense to me from an artist's perspective: a larger aperture ( AA ) directly results in a larger CoC. Simple and effective.


Controlling the Blur Range with a Focus Region

Next, I needed to define a "focus region" to control the extent of the blur. If you only blur based on the exact focus distance, the in-focus area can feel too narrow and sharp.

To solve this, I implemented a simple range check in the shader:

check=DF<Region2.0 check = |D - F| < \frac{Region}{2.0}
  • DD : Scene Depth
  • FF : Focus Distance
  • RegionRegion : The width of the area to be considered in focus

If this check is true, the pixel is within the focus region, and no blur is applied. For pixels outside this range, we apply the blur based on our CoC calculation. This gives us clear control over what's sharp and what's blurry.


Implementing Disk Sampling in HLSL with the Golden Angle

To create the blur effect, we need to sample surrounding pixels in a circular pattern—a technique called "disk blur." To do this efficiently and uniformly, I used a method based on the Golden Angle.

Using the Golden Angle ensures that our sample points are evenly distributed, avoiding clumps or artifacts, even with a low sample count. It's the same principle that nature uses to arrange seeds in a sunflower head.

Here is the HLSL function to generate these sample points:

#define GOLDEN_ANGLE 2.3999632297 // (2 * PI) / (Golden Ratio^2)

/**
* Generates a sample point on a disk using the Golden Angle.
* @param Index The current sample index (starting from 0).
* @param NumSamples The total number of samples.
* @return A 2D coordinate normalized to the [-1, 1] range.
*/
float2 GetRandomDiskSample(float Index, float NumSamples)
{
float Radius = sqrt(Index / NumSamples);
float Theta = Index * GOLDEN_ANGLE;

float2 SamplePoint;
SamplePoint.x = Radius * cos(Theta);
SamplePoint.y = Radius * sin(Theta);

return SamplePoint;
}
Enter fullscreen mode Exit fullscreen mode

The sqrt(Index / NumSamples) part distributes the samples from the center outwards, while Index * GOLDEN_ANGLE rotates each point, ensuring they don't overlap and cover the disk uniformly.


The Final Shader Code and Applying a Bokeh Shape

Putting it all together, here is the core logic of the final shader. I also added an option to use a texture to define the bokeh shape for a more realistic effect.

// Struct to hold our functions
struct Functions
{
float2 GetRandomDiskSample(float Index, float NumSamples)
{
float Radius = sqrt(Index / NumSamples);
float Theta = Index * 2.3999632297; // GOLDEN_ANGLE

float2 SamplePoint;
SamplePoint.x = Radius * cos(Theta);
SamplePoint.y = Radius * sin(Theta);

return SamplePoint;
}
};
Functions f;

// Calculate the blur radius for this pixel from the CoC value (0-1) and max blur size
float CoC_Radius = CoC_Value * MaxBlurSize;

float4 FinalColor = float4(0, 0, 0, 0);
float TotalWeight = 0;

// Sample surrounding pixels
for (int i = 0; i < NumSamples; i++)
{
// 1. Generate an offset coordinate for sampling
float2 Offset = f.GetRandomDiskSample(i, NumSamples) * CoC_Radius;

// 2. Fetch the color from the scene texture
float4 SampledColor = SceneTextureFetch(SceneTex.ID, UV + Offset);

// 3. (Optional) Calculate weight using a bokeh shape texture
// Convert offset from [-1, 1] to UV coordinates [0, 1]
float2 BokehUV = (Offset / CoC_Radius) * 0.5 + 0.5;

// Sample the bokeh shape texture and use its intensity as a weight
float BokehWeight = length(BokehShapeTex.Sample(BokehShapeTexSampler, BokehUV));

// 4. Add the weighted color to the final color
FinalColor += SampledColor * BokehWeight;
TotalWeight += BokehWeight;
}

// Calculate the weighted average to get the final color
if (TotalWeight > 0)
{
FinalColor /= TotalWeight;
}

return FinalColor;
Enter fullscreen mode Exit fullscreen mode

I tried to replicate the hexagonal shape of an aperture blade, similar to Unreal Engine's default DoF, by using the BokehShapeTex to weight the samples.

However, to be honest, using a bokeh shape texture didn't make a huge visual impact. The shape is hard to discern with a low sample count or a small blur radius. For performance-critical applications, you can probably skip this step without losing much.


Conclusion

This was a fun experiment in creating a custom DoF effect using post-process materials and HLSL. Here are the key takeaways:

  • Modified the CoC formula to be more artist-friendly than the physically-based version.
  • Implemented a focus region to gain precise control over the blurred and sharp areas.
  • Used the Golden Angle for disk sampling to achieve a clean and efficient blur.

I hope this breakdown is helpful for anyone else looking to expand their rendering skills!

About the Assets

All assets used in the video were created using free (at the time) assets that I've collected over the years.

About the YouTube Video

In addition to the custom DoF effect, the final video also includes adjustments for Color Grading and Chromatic Aberration to achieve the final look.

Recent screenshot

Even close-ups have a nice miniature feel to them.

Top comments (0)