Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 209 additions & 27 deletions crates/bevy_pbr/src/volumetric_fog/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ pub struct VolumetricFogUniform {
density_texture_offset: Vec3,
scattering_asymmetry: f32,
light_intensity: f32,
boundary_fade: f32,
jitter_strength: f32,
}

Expand Down Expand Up @@ -707,7 +708,6 @@ pub fn prepare_volumetric_fog_uniforms(

for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() {
let world_from_view = extracted_view.world_from_view.affine();

let mut view_fog_volumes = vec![];

for ((_, fog_volume, _), local_from_world) in
Expand All @@ -719,7 +719,9 @@ pub fn prepare_volumetric_fog_uniforms(

// Determine whether the camera is inside or outside the volume, and
// calculate the clip space transform.
let interior = camera_is_inside_fog_volume(&local_from_view);
let z_near = extracted_view.clip_from_view.w_axis[2];
let interior = camera_is_inside_fog_volume(&local_from_view, z_near);
let distance_to_border = fog_volume_boundary_distance(&local_from_view);
let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms(
interior,
&extracted_view.clip_from_view,
Expand Down Expand Up @@ -748,6 +750,11 @@ pub fn prepare_volumetric_fog_uniforms(
density_texture_offset: fog_volume.density_texture_offset,
scattering_asymmetry: fog_volume.scattering_asymmetry,
light_intensity: fog_volume.light_intensity,
boundary_fade: calculate_fog_volume_boundary_fade(
&local_from_view,
interior,
distance_to_border,
),
jitter_strength: volumetric_fog.jitter,
});

Expand Down Expand Up @@ -782,31 +789,36 @@ pub fn prepare_view_depth_textures_for_volumetric_fog(
}

fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] {
let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0);

for &local_normal in &[
Vec3A::X,
Vec3A::NEG_X,
Vec3A::Y,
Vec3A::NEG_Y,
Vec3A::Z,
Vec3A::NEG_Z,
] {
let view_normal = view_from_local
.transform_vector3a(local_normal)
let mut far_planes = [Vec4::ZERO; 3];

// Iterate the three axis-aligned face pairs of the unit cube.
// For each pair, always pick exactly one face — the one whose view-space
// normal is more front-facing (larger z component).
for (i, &(pos_normal, neg_normal)) in [
(Vec3A::X, Vec3A::NEG_X),
(Vec3A::Y, Vec3A::NEG_Y),
(Vec3A::Z, Vec3A::NEG_Z),
]
.iter()
.enumerate()
{
let pos_view = view_from_local
.transform_vector3a(pos_normal)
.normalize_or_zero();
let neg_view = view_from_local
.transform_vector3a(neg_normal)
.normalize_or_zero();
if view_normal.z <= 0.0 {
continue;
}

let view_position = view_from_local.transform_point3a(-local_normal * 0.5);
let plane_coords = view_normal.extend(-view_normal.dot(view_position));
// Pick the face whose view-space normal has the larger z (more
// front-facing).
let (local_normal, view_normal) = if pos_view.z >= neg_view.z {
(pos_normal, pos_view)
} else {
(neg_normal, neg_view)
};

far_planes[next_index] = plane_coords;
next_index += 1;
if next_index == far_planes.len() {
continue;
}
let view_position = view_from_local.transform_point3a(local_normal * 0.5);
far_planes[i] = view_normal.extend(-view_normal.dot(view_position));
}

far_planes
Expand Down Expand Up @@ -839,15 +851,94 @@ impl VolumetricFogBindGroupLayoutKey {
}

/// Given the transform from the view to the 1×1×1 cube in local fog volume
/// space, returns true if the camera is inside the volume.
fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool {
/// space, returns true if the camera is inside (or within near-clip distance
/// of) the volume. Expanding by `z_near` ensures we switch to interior
/// rendering just before the near plane clips the front-face geometry, making
/// the depth continuous across the mode switch.
fn camera_is_inside_fog_volume(local_from_view: &Affine3A, z_near: f32) -> bool {
// Use the full near-clip distance in local space as a uniform expansion.
let near_clip_extent = (local_from_view.matrix3 * Vec3A::Z).length() * z_near;
local_from_view
.translation
.abs()
.cmple(Vec3A::splat(0.5))
.cmple(Vec3A::splat(0.5 + near_clip_extent))
.all()
}

fn fog_volume_boundary_distance(local_from_view: &Affine3A) -> f32 {
0.5 - local_from_view.translation.abs().max_element()
}

/// Computes the distance from the camera to the first exit point from the
/// fog volume along the camera forward direction.
fn fog_exit_distance_along_view(local_from_view: &Affine3A) -> f32 {
let camera_pos = local_from_view.translation;
let camera_forward_local = (local_from_view.matrix3 * Vec3A::NEG_Z).normalize_or_zero();

// Compute intersections with the six faces of the unit cube (±0.5).
let mut closest_t = 1e30_f32;

for &(axis, face) in &[
(0, 0.5),
(0, -0.5),
(1, 0.5),
(1, -0.5),
(2, 0.5),
(2, -0.5),
] {
let denom = camera_forward_local[axis];
if denom.abs() < 1e-6 {
continue;
}
let t = (face - camera_pos[axis]) / denom;
if t > 0.0 {
let hit = camera_pos + camera_forward_local * t;
let other_axes = match axis {
0 => (1, 2),
1 => (0, 2),
_ => (0, 1),
};
if hit[other_axes.0].abs() <= 0.5 && hit[other_axes.1].abs() <= 0.5 {
closest_t = closest_t.min(t);
}
}
}

closest_t.max(0.1)
}

/// Returns a fade factor based on proximity to the volume boundary.
fn calculate_fog_volume_boundary_fade(
local_from_view: &Affine3A,
interior: bool,
distance_to_border: f32,
) -> f32 {
if !interior {
return 1.0;
}

let center_to_camera_dir_local = local_from_view.translation.normalize_or_zero();
let camera_forward_local = (local_from_view.matrix3 * Vec3A::NEG_Z).normalize_or_zero();
let outward_view_alignment = camera_forward_local.dot(center_to_camera_dir_local);
let is_looking_toward_volume_center = outward_view_alignment < 0.0;
if is_looking_toward_volume_center {
return 1.0;
}

// Use directional exit distance through the volume along view direction.
let fade_extent = fog_exit_distance_along_view(local_from_view);

if fade_extent <= 0.0 {
return 1.0;
}

let t = (distance_to_border / fade_extent).clamp(0.0, 1.0);
let base_fade = t * t * (3.0 - 2.0 * t);

let outward_weight = outward_view_alignment.clamp(0.0, 1.0);
1.0 - outward_weight * (1.0 - base_fade)
}

/// Given the local transforms, returns the matrix that transforms model space
/// to clip space.
fn calculate_fog_volume_clip_from_local_transforms(
Expand All @@ -871,3 +962,94 @@ fn calculate_fog_volume_clip_from_local_transforms(
vec4(0.0, 0.0, z_near, z_near),
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn camera_inside_without_near_clip_expansion_matches_box_test() {
let mut local_from_view = Affine3A::IDENTITY;
local_from_view.translation = Vec3A::new(0.5, 0.5, 0.5);
assert!(camera_is_inside_fog_volume(&local_from_view, 0.0));

local_from_view.translation = Vec3A::new(0.5001, 0.0, 0.0);
assert!(!camera_is_inside_fog_volume(&local_from_view, 0.0));
}

#[test]
fn camera_inside_with_near_clip_expansion_includes_boundary_margin() {
let mut local_from_view = Affine3A::IDENTITY;

// For identity transform, near_clip_extent == z_near.
local_from_view.translation = Vec3A::new(0.55, 0.0, 0.0);
assert!(camera_is_inside_fog_volume(&local_from_view, 0.05));

local_from_view.translation = Vec3A::new(0.56, 0.0, 0.0);
assert!(!camera_is_inside_fog_volume(&local_from_view, 0.05));
}

#[test]
fn fog_volume_boundary_distance_is_signed_distance_to_surface() {
let mut local_from_view = Affine3A::IDENTITY;

local_from_view.translation = Vec3A::new(0.0, 0.0, 0.0);
assert_eq!(fog_volume_boundary_distance(&local_from_view), 0.5);

local_from_view.translation = Vec3A::new(0.5, 0.0, 0.0);
assert_eq!(fog_volume_boundary_distance(&local_from_view), 0.0);

local_from_view.translation = Vec3A::new(0.6, 0.0, 0.0);
assert!((fog_volume_boundary_distance(&local_from_view) + 0.1).abs() < 1e-6);
}

#[test]
fn fog_exit_distance_along_view_hits_unit_cube_faces() {
let mut local_from_view = Affine3A::IDENTITY;

local_from_view.translation = Vec3A::ZERO;
assert_eq!(fog_exit_distance_along_view(&local_from_view), 0.5);

local_from_view.translation = Vec3A::new(0.0, 0.0, 0.25);
assert_eq!(fog_exit_distance_along_view(&local_from_view), 0.75);
}

#[test]
fn boundary_fade_is_full_outside_and_tapers_inside() {
let mut local_from_view = Affine3A::IDENTITY;

assert_eq!(
calculate_fog_volume_boundary_fade(&local_from_view, false, 0.5),
1.0
);

// Place camera on -Z axis so default forward (-Z) is outward.
local_from_view.translation = Vec3A::new(0.0, 0.0, -0.4);
assert_eq!(
calculate_fog_volume_boundary_fade(&local_from_view, true, 0.1),
1.0
);

local_from_view.translation = Vec3A::new(0.0, 0.0, -0.5);
assert_eq!(
calculate_fog_volume_boundary_fade(&local_from_view, true, 0.0),
0.0
);

// Looking back toward the center disables the fade.
local_from_view.translation = Vec3A::new(0.0, 0.0, 0.4);
assert_eq!(
calculate_fog_volume_boundary_fade(&local_from_view, true, 0.1),
1.0
);
}

#[test]
fn get_far_planes_identity_selects_three_axis_planes() {
let far_planes = get_far_planes(&Affine3A::IDENTITY);

assert_eq!(far_planes[0], Vec4::new(1.0, 0.0, 0.0, -0.5));
assert_eq!(far_planes[1], Vec4::new(0.0, 1.0, 0.0, -0.5));
assert_eq!(far_planes[2], Vec4::new(0.0, 0.0, 1.0, -0.5));
}
}
Loading