Tech-Art
Case study
Intermediate
Tutorial
Volumetric fog without the raymarching cost
10

Volumetric fog usually requires complex algorithms that raymarch through 3D space, evaluate shadow maps, and cache 3D data. However, this is true for systems that need to be universal and "automatic"
There is another option that has existed since the beginning of realtime computer graphics: fake it.
I will show you how I improved Unity's built-in fog by adding volumetric blobs around lights.
In this article:
I prototype the effect with additively blended spheres
I showcase the hard scene and near-plane intersection issues
I calculate analytical sphere density in a fragment shader
I clip the effect against the camera and scene depth
I assemble the final scene
___
Effect idea
Look at these photos. They were taken in fog.

Notice how each light illuminates the surrounding air. I imagine each light as a volumetric blob that lights the fog.

___
Original scene
I will recreate that effect in the GardenScene from Unity's URP Template.

I enabled exponential fog under Window > Rendering > Environment:

This is how the scene looks with fog. It creates a convincing foggy-night atmosphere.

But something is missing here. The lights don't illuminate the fog around them:

___
Implementing the volumetric fog
Let's fix it. My idea is to place a sphere at each light and use it to illuminate the fog. I will use additively rendered spheres as a cheap substitute for volumetric fog.
Let's start from a prototype.
Prototype
Let's test whether an additively rendered sphere works here.
I added a sphere with an unlit shader and placed it at the light's position.
Then I used a Fresnel effect to darken the sphere's edges based on the angle between the view direction and the sphere normal:

Then I switched to additive blending with Blend One One:

It works and looks acceptable, though it has a few artifacts. I reduced the light's intensity:

I placed a sphere like this at each light in the scene:

I adjusted the effect's intensity. This is the result with the effect off and on:
It is simple and works. There are still issues to solve, but this prototype proves that the method can improve the mood and add a volumetric touch.
___
Current issues
However, I cannot use the current implementation because it has two important issues.
First issue - hard intersections with the scene. It is just a spherical mesh.

I could disable depth testing, but then the blobs would be visible through opaque obstacles.
Second issue - camera near plane intersections
When the camera passes through a sphere, it disappears as soon as its triangles move behind the camera. This would not be an issue in a game where the camera never enters the spheres. Here, it does.
Analytical approach
I need a solution that accounts for:
The scene depth of opaque objects
The current camera position, so the effect fades when the camera is inside the sphere and looks outward
My second idea is to compute the fog analytically in the fragment shader.
I will render the back faces of the sphere.

Then I will create a ray from the camera position and view direction. Its intersections with the sphere provide the entry and exit points.

I will assume that the distance between those points is proportional to the "fog" I want to render.

This makes the fog more intense when looking through the center of the sphere:

This analytical method detects when the camera is inside the sphere and calculates the distance from the camera to its surface. It removes the camera-triangle intersection issue, so the effect fades as the camera moves through the sphere.
To hide scene intersections, I can use the depth buffer to adjust the other end of the segment.

___
Analytical implementation
The next steps explain how I implemented this effect.
1. Unity setup
First, I need access to the depth buffer. I will ensure that Unity renders it so I can sample it later during transparent rendering.
I enabled it in the URP Renderer Asset:

I also used the Frame Debugger to confirm that Unity renders depth to the texture:

2. Shader setup
I created a transparent unlit URP shader that renders back faces without depth testing or depth writes. This excerpt shows the relevant ShaderLab configuration:

3. Vertex shader and interpolators
The vertex shader is simple. It outputs the world-space and clip-space positions, which are all I need to calculate the sphere intersection and sample the scene depth.
I also tested whether the interpolators work correctly.

In the next steps, I will prepare all the data needed to calculate the effect:
Ray origin and direction
Sphere position and radius
Distance to the scene
4. Ray reconstruction
I use the current camera position and the world-space position of the sphere's surface to calculate the ray.
Note: This ray reconstruction assumes a perspective camera. An orthographic camera needs a per-pixel ray origin on the near plane and a direction based on the camera's forward vector.
I usually check calculated values by displaying them as colors on the object. In this case, rayDirectionWS should match the colors of the world-space axes.
5. Getting the sphere position and radius
I will use the model matrix to get the sphere's radius and position. This matrix transforms the mesh from object space to world space, so it contains its offset and scale.
The object's position is stored in the matrix's fourth column. The X-axis scale equals the length of the vector in the first column.
Note: I assume that the source sphere has a diameter of one unit and uses a uniform scale, with the same value on each axis.
I tested this visually. The sphere gets brighter as I make it larger.
6. Scene depth
The final ingredient is scene depth. In Unity, I can access the depth texture by including this HLSL file:
It provides the SampleSceneDepth(float2 screenUV) method for sampling the raw depth value. The URP shader template also includes Core.hlsl, which provides the reconstruction helpers used below.
I will calculate the world-space distance to the scene by reconstructing the opaque surface's world-space position from the raw depth value.
A depth value is created by transforming a world-space position into clip space with the view-projection matrix. URP's ComputeWorldSpacePosition helper uses the inverse view-projection matrix to convert the sampled depth back to world space:
Note: This example targets a standard non-XR camera. For XR, use URP's stereo-aware screen-space and texture-sampling macros. Pixels without opaque geometry use the far-plane depth, so they do not shorten the sphere segment.
7. Sphere intersections
Now I have everything needed to calculate the effect:
Ray origin and direction
Sphere position and radius
Distance to the scene
I will start by creating a function called SphereDensity. It takes these parameters and returns the sphere's fog density.
I will start by calculating the intersections. I used a formula from Inigo Quilez's collection of intersection functions.
This part is now complete:

This is how it looks:

This is not the smooth result I expected, so I will adjust it with a pow function.

8. Clamping to camera position and scene depth
Now I need to handle cases where the camera or an opaque surface is inside the sphere. I will clamp the segment's start or end.

Because sphere intersections are measured as distances along the ray, I need to ensure that the start is never less than 0.
For the scene, I can clamp the farther intersection to the scene distance.
It works, but something looks wrong. The effect appears close to the sphere's outer shell.
The problem is the pow(..., 4.0) expression that smooths the sphere. It makes the distance scale nonlinearly. When the scene depth or camera shortens the calculated distance, the change is nonlinear, but it should be linear.
9. Fixing the math
I want to keep the nonlinear calculation for the sphere while making the camera and scene affect it linearly.
My idea is to calculate the ratio between the original and clamped segments, then use it to scale the fog intensity.

Here is the implementation:
This is how it looks. All the issues are now fixed.
10. Assembling the scene
The final step is assembling the scene. I placed about 42 spheres with this shader and adjusted their intensity.
___
Result
Here is the final result:
Before:

After:

___
Summary
This effect fakes local volumetric fog with additively blended spheres. The fragment shader intersects the camera ray with each sphere, clips the segment against the camera and scene depth, then shapes the result into a smooth density. This avoids hard scene and near-plane intersections without raymarching through a volume.
The sphere intersection is based on Inigo Quilez's analytical intersection functions.
In the next article, I will show you how I optimize this effect.


