#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);
}