Banded Depth Fog (+ Low Poly Fog)

This is a shader for people who hate gradients and curves for some inexplicable reason.
I made this for “low poly fog”, but you can also use it for regular circular banding if you set the polygon_edges to 2 and polygon_planar_faces to 0.  Setting polygon_planar_faces
 to 1 makes the bands truly low poly! with no curves.

You can also have polygons with curved edges, giving a cool retro-future vibe.
And you can have the banding be completely linear with no curve if you set fov_curve_strength to 0 and and polygon_planar_faces to 0. (however that looks ugly if you’re not doing a fixed perspective; it may have some use).

Just mess around with the settings and see what works for you.

To use it, apply it to a quad mesh and throw it in front of your camera. (also note the vertex() function)

Rendering After Transperent Meshes:
As with all postprocessing shaders that require hint_depth_texture, you cannot see transperent objects through them due to the render order and there is no easy workaround for this because depth maps aren’t accessible from subviewports , subviewports being the only way to do segmented rendering in 3D.
The more difficult workaround for this was to create a compute shader for this, which I have managed to do: I have posted a comment below on walking you through implementing the compute shader version.

Shader code
shader_type spatial;
render_mode unshaded, cull_disabled, depth_draw_never, depth_prepass_alpha;

// -- Inputs / tuning --
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture;
uniform vec4 fog_color : source_color = vec4(0.6, 0.7, 0.8, 1.0);

//These must be equal to camera settings:
uniform float camera_near : hint_range(0.01, 10.0) = 0.1;
uniform float camera_far = 4000.0;
uniform float camera_fov_deg : hint_range(1.0,179.0) = 75.0;




// Banding / polygon parameters
uniform int   band_count : hint_range(1, 999) = 8;
uniform float band_density   : hint_range(0.0, 100.0) = 15.0;

uniform int  polygon_edges  : hint_range(2, 32) = 6;      // number of straight edges (3..32) (2 = disabled)
uniform float polygon_round  : hint_range(0.0, 1.0) = 0.0;  // 0 = sharp edges, >0 = rounded corners

// Global fog strength
uniform float fog_strength : hint_range(0.0, 1.0) = 1.0;

uniform float fov_curve_strength : hint_range(0.0,1.0) = 1.0;

const int   MAX_EDGES = 32;

// 0 = keep current curved bands; 1 = fully planar faces (no bending on angled surfaces)
uniform float polygon_planar_faces : hint_range(0.0, 1.0) = 1.0;

// How fast bands shrink with distance in planar mode (0 = straight prism, larger = tighter pyramid)
uniform float polygon_aperture_deg : hint_range(0.0, 85.0) = 40.0;

// 0 = rotate with camera (screen-locked), 1 = rotate with world (world-locked)
uniform bool polygon_lock_world = true;

// Extra rotation offset in degrees (applied after locking mode is chosen)
uniform float polygon_rotation_deg : hint_range(-180.0, 180.0) = 0.0;

// -- Helpers --

// 2D rotate
vec2 rot2(vec2 v, float a) {
    float c = cos(a), s = sin(a);
    return vec2(c * v.x - s * v.y, s * v.x + c * v.y);
}

// World-space yaw of the camera (radians), using the camera's right axis projected to XZ.
// Requires _VIEW_MATRIX (available in spatial shaders).
float camera_yaw(mat4 _INV_VIEW_MATRIX) {
	
    return atan(_INV_VIEW_MATRIX[2].x, -_INV_VIEW_MATRIX[2].z);
}

// World-space distance between adjacent band steps
float band_step_world() {
    return camera_far / max(1.0, band_density * float(band_count));
}

// Smooth max; k=0 => hard max, higher k => rounder corners
float smooth_max(float a, float b, float k) {
    if (k <= 0.0) return max(a, b);
    float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0);
    return mix(b, a, h) + k * h * (1.0 - h);
}

// Polygon support with explicit angle offset (for rotation control)
float polygonal_support_rot(vec2 q, int edges, float roundness, float angle_offset) {
    if (edges < 3) {
        return length(q); // fallback: circular
    }
    float step_ang = 2.0 * PI / float(edges);
    float c = cos(PI / float(edges));
    float k = roundness * 0.35; // curved (tan-space) scale

    float m = -1e9;
    for (int i = 0; i < MAX_EDGES; i++) {
        if (i >= edges) break;
        float a = angle_offset + step_ang * float(i);
        vec2 n = vec2(cos(a), sin(a));
        float v = dot(q, n) / c;
        m = (i == 0) ? v : smooth_max(m, v, k);
    }
    return m;
}



// Planar-faced "polyhedral" measure: level sets are unions of planes (no curvature).
// slope = tan(aperture), controls how fast bands shrink with depth (0 = vertical prisms).
float polyhedral_measure_oriented(vec3 view_pos, int edges, float roundness, float slope, float angle_offset) {
    if (edges < 3) {
        return view_pos.z;
    }
    float c = cos(PI / float(edges));
    float step_ang = 2.0 * PI / float(edges);

    // Scale smoothing by band thickness for visibility (planar mode)
    float k = (roundness <= 0.0) ? 0.0 : roundness * band_step_world() / c;

    // Choose 2D coords for polygon orientation
    vec2 q = view_pos.xy;  // camera right/up plane
    

    float m = -1e9;
    for (int i = 0; i < MAX_EDGES; i++) {
        if (i >= edges) break;
        float a = angle_offset + step_ang * float(i);
        vec2 n = vec2(cos(a), sin(a));
        float v = (dot(q, n) + slope * view_pos.z) / c;
        m = (i == 0) ? v : smooth_max(m, v, k);
    }
    return m;
}


// Compute polygonal "radius" in projection plane.
// q is in projection-plane units (tan-space), as you already use for circular correction.
// edges < 3 falls back to circular.
float polygonal_radius(vec2 q, int edges, float roundness) {
    if (edges < 3) {
        return length(q); // disabled / circular
    }

    // angle between outward normals
    float step_ang = 2.0 * PI / float(edges);
    // cos(pi/N) is the inradius of a unit-circumradius polygon; used to normalize
    float c = cos(PI / float(edges));

    // roundness mapped to a smoothing width; tweak as you like
    // Values ~0.0..0.4 work well for most FOVs
    float k = roundness * 0.35;

    // Accumulate smooth max of dot(q, n_i)/c over all edge normals
    float m = -1e9;
    for (int i = 0; i < MAX_EDGES; i++) {
        if (i >= edges) break;
        float a = step_ang * float(i);
        vec2 n = vec2(cos(a), sin(a));
        float v = dot(q, n) / c;
        m = (i == 0) ? v : smooth_max(m, v, k);
    }
    return m;
}

// Curved (FOV) multiplier with rotation control.
// angle_offset is in radians; for world-lock set it to (user_rot - camera_yaw).
float fov_ray_multiplier_polygon_rot(vec2 uv, float fov_deg, float aspect, int edges, float roundness, float angle_offset) {
    // ndc [-1,1]
    vec2 ndc = uv * 2.0 - vec2(1.0);

    float fov_rad = radians(fov_deg);
    float tan_y = tan(fov_rad * 0.5);
    float tan_x = tan_y * aspect;

    // projection-plane coords (tan-space)
    vec2 q = ndc * vec2(tan_x, tan_y);

    float r = (edges < 3) ? length(q) : polygonal_support_rot(q, edges, roundness, angle_offset);

    // ray_length = view_depth * sqrt(1 + r^2)
    return sqrt(1.0 + r * r);
}




float linearize_depth(float z_sample) {
	float z_ndc = z_sample * 2.0 - 1.0;
	return (2.0 * camera_near * camera_far) / (camera_far + camera_near - z_ndc * (camera_far - camera_near));
}



//You can use this to fill the screen of any camera that approaches the quad automatically
void vertex() { POSITION = vec4(VERTEX.xy, 1.0, 1.0); }

void fragment() {
    vec2 uv = SCREEN_UV;

    vec4 scene = texture(DEPTH_TEXTURE, uv);
    float depth = 1.0 - scene.r;
    float view_depth = linearize_depth(depth);

    float aspect = VIEWPORT_SIZE.x / max(1.0, VIEWPORT_SIZE.y);

    // Rotation controls
    float user_rot = radians(polygon_rotation_deg);

    // 1) Curved (screen/camera plane), with optional world-lock by cancelling camera yaw
    float angle = user_rot;
    if (polygon_lock_world) {
		
		
		
        angle -= camera_yaw(INV_VIEW_MATRIX); // keep polygon aligned to world while camera yaws
    }
    float mult_curved = fov_ray_multiplier_polygon_rot(uv, camera_fov_deg, aspect,
                                                       polygon_edges, polygon_round, angle);
    float curved_measure = mix(view_depth, view_depth * mult_curved, clamp(fov_curve_strength, 0.0, 1.0));

    // 2) Planar-faced (no curvature), with world/camera lock and rotation
    vec2 ndc = uv * 2.0 - vec2(1.0);
    float fov_rad = radians(camera_fov_deg);
    float tan_y = tan(fov_rad * 0.5);
    float tan_x = tan_y * aspect;
    vec3 view_pos = vec3(ndc * vec2(tan_x, tan_y) * view_depth, view_depth);

    float slope = tan(radians(polygon_aperture_deg)); // 0 => prisms, >0 => pyramid
    float planar_measure = polyhedral_measure_oriented(view_pos, polygon_edges, polygon_round, slope, angle);
	
    // Blend between curved and planar-faced
    float effective_measure = mix(curved_measure, planar_measure, clamp(polygon_planar_faces, 0.0, 1.0));
	
    // Banding
    float norm_eff = clamp(effective_measure / camera_far, 0.0, 1.0) * band_density;
    float depth_norm = clamp(norm_eff, 0.0, 1.0);
    depth_norm *= float(band_count);
    depth_norm = floor(depth_norm);
    depth_norm /= float(band_count);

    vec3 out_color = fog_color.rgb;
    ALBEDO = out_color;
    ALPHA  = depth_norm * fog_strength;
}
Tags
Fog, Low Poly
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from KnightNine

3D Low Distortion Refraction (Low Poly Glass)

Low Poly Waterfalls

Low Poly Fresnel

Related shaders

3D Low Distortion Refraction (Low Poly Glass)

Linear Depth/Depth Fog

Low Poly Waterfalls

guest

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments