Volumetric Fog Rendering with Froxels (OpenGL)

(Honours project, November 2021 - May 2022)

Demo video of the application built for my honours project.

Description

For my honours project in the fourth year of my undergraduate, I chose to tackle volumetric lighting via frustum-aligned voxels (froxels, for short). Over the course of this research module I learned a great deal about volumetric rendering and building critical analysis skills when looking at resources while researching like academic papers and SIGGRAPH/GDC presentations.

My research involved comparing differences across volumetric fog implementations like Wronski’s original implementation, NVIDIA’s tessellated mesh method and Kovalovs’ improvements on the froxel method for The Last of Us Part II, then comparing the impact each difference in implementation had on the computational latency, memory footprint and visual results of the final application.

Project Components

How Froxels Work:

Rather than the previous method of achieving volumetric lighting, where raymarching is performed in view space to accumulate scattered light from the camera to points being shaded, this project makes use of a froxel-based method of evaluating light scattering in the camera’s view.
A froxel refers to a frustum-shaped voxel, which the camera frustum is split up while rendering a frame to evaluate light scattering at discrete positions with a compute shader. The results at each froxel are stored in a corresponding texel of a 3D texture, ready to be applied to the shaded objects in the scene.

The camera frustum is split up into froxels, where scattered light calculations are performed at each froxel’s position and stored in a 3D texture ready to be compositied onto the final scene.

Reducing Noise with Jittering and Temporal Blending:

Because the camera frustum is split up into many froxels and stored in a 3D texture, the resolution of said texture has to be quite low to maintain realtime performance. Because of this, if the fog results are used as-is then the final result will be prone to ugly colour banding.
To alleviate this, the froxel positions are offset along the camera-to-froxel vector by a different amount each frame, which turns the banding into noise. This project uses Halton sequences to offset the froxel positions each frame, though other methods like blue noise textures work as well.

Offsetting the froxel positions each frame turns colour banding caused by the 3D texture’s low resolution into noise.

Creating noise like this in the final 3D texture is actually very useful, because noise can be filtered with methods like temporal blending. This project makes use of two 3D textures, with the compute shader alternating between which texture is written to each frame. Once the fog results have been found, the 3D texture used in the previous frame has its results blended with the results found in the current frame, drastically reducing the noise and flickering in the final result. It’s important to make sure that the fog results at the same position between frames are used though, so the froxel position for the current frame is reprojected into the previous frame to figure out where in the previous frame’s 3D texture to sample from.

Blending the fog results of both the current and previous frame drastically reduces the noise and produces stable results without excessive banding.

Profiling and Gathering Performance Metrics:

Because my honours project was research-based, I incorporated Nvidia Nsight Perf SDK into the codebase which enabled me to capture performance data in incredible detail, including the activity levels of specific hardware units and reasons for GPU stalls like branching and waiting on warp allocation.

An excerpt of a performance report produced by Nvidia Nsight Perf SDK, detailing low-level metrics including stall reasons and cycles elapsed.