bevy_exponential_height_fog 0.1.0

Standalone exponential height fog extension for Bevy volumetric fog.
Documentation
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::mesh_view_bindings::view
#import bevy_pbr::view_transformations::{frag_coord_to_ndc, position_ndc_to_world}

struct ExponentialHeightFogParams {
    fog_color: vec4<f32>,
    primary_params: vec4<f32>,
    secondary_params: vec4<f32>,
    distance_params: vec4<f32>,
    distance_reference_params: vec4<f32>,
    optimization_params: vec4<f32>,
}

// @group(0) @binding(0) is mesh_view_bindings::view.

#ifdef MULTISAMPLED
@group(0) @binding(1) var depth_texture: texture_depth_multisampled_2d;
#else
@group(0) @binding(1) var depth_texture: texture_depth_2d;
#endif

@group(0) @binding(2) var color_texture: texture_2d<f32>;
@group(0) @binding(3) var color_sampler: sampler;
@group(0) @binding(4) var<uniform> fog: ExponentialHeightFogParams;

fn layer_density(world_height: f32, layer: vec4<f32>) -> f32 {
    let density = layer.x;
    let height_falloff = layer.y;
    let height_offset = layer.z;
    let enabled = layer.w;
    if (enabled < 0.5 || density <= 0.0) {
        return 0.0;
    }

    // Clamp exponent to avoid exp overflow while preserving smooth behavior.
    let exponent = clamp(-(world_height - height_offset) * max(height_falloff, 0.00001), -80.0, 80.0);
    return density * exp(exponent);
}

fn distance_weight(distance_to_camera: f32, params: vec4<f32>) -> f32 {
    let start_distance = params.x;
    let cutoff_distance = params.y;
    let fade_width = max(params.z, 0.001);
    var weight = 1.0;

    if (start_distance > 0.0) {
        weight *= smoothstep(start_distance, start_distance + fade_width, distance_to_camera);
    }
    if (cutoff_distance > 0.0 && cutoff_distance > start_distance) {
        weight *= 1.0 - smoothstep(cutoff_distance - fade_width, cutoff_distance, distance_to_camera);
    }
    return clamp(weight, 0.0, 1.0);
}

@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
    let screen_size = vec2<f32>(textureDimensions(color_texture));
    let uv = in.position.xy / screen_size;
    let source = textureSampleLevel(color_texture, color_sampler, uv, 0.0);

    if (fog.optimization_params.w < 0.5) {
        return source;
    }

    let frag_coord = vec2<i32>(floor(in.position.xy));
    let depth = textureLoad(depth_texture, frag_coord, 0);
    if (depth >= fog.optimization_params.z) {
        return source;
    }

    let ndc = frag_coord_to_ndc(vec4<f32>(in.position.xy, depth, 1.0));
    let world_position = position_ndc_to_world(ndc);
    var distance_reference_world_position = view.world_position.xyz;
    if (fog.distance_reference_params.w > 0.5) {
        distance_reference_world_position = fog.distance_reference_params.xyz;
    }
    let camera_distance = distance(world_position, distance_reference_world_position);

    let primary = layer_density(world_position.z, fog.primary_params);
    let secondary = layer_density(world_position.z, fog.secondary_params);
    let extinction = max(primary + secondary, 0.0) * distance_weight(camera_distance, fog.distance_params);
    var fog_alpha = 1.0 - exp(-extinction);
    fog_alpha = clamp(fog_alpha, 0.0, fog.distance_params.w);
    fog_alpha = max(fog_alpha, fog.optimization_params.x);

    if (fog_alpha < fog.optimization_params.y) {
        return source;
    }

    let fogged_rgb = mix(source.rgb, fog.fog_color.rgb, fog_alpha);
    return vec4<f32>(fogged_rgb, source.a);
}