Skip to content

Disable SSAO for meshes without depth writes - #25358

Open
venhelhardt wants to merge 1 commit into
bevyengine:mainfrom
venhelhardt:ssao-depth-write
Open

Disable SSAO for meshes without depth writes#25358
venhelhardt wants to merge 1 commit into
bevyengine:mainfrom
venhelhardt:ssao-depth-write

Conversation

@venhelhardt

@venhelhardt venhelhardt commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Objective

Screen-space ambient occlusion can produce artifacts for meshes that do not contribute to the depth prepass. SSAO may still be sampled for these meshes even though no corresponding depth information was written, potentially causing invalid or uninitialized depth data to affect the result.

This primarily affects transparent meshes and other cases where depth writes are disabled.

Solution

Only enable screen-space ambient occlusion for meshes with depth writes enabled.

Since depth-write behavior depends on multiple pipeline properties, use the already resolved depth_write_enabled value when configuring the mesh pipeline rather than inferring it from individual material or pipeline flags.

Testing

  • Did you test these changes? If so, how?
    cargo run --features free_camera --example ssao (modified)
    cargo run -p ci
  • Are there any parts that need more testing? I am not sure.
  • How can other people (reviewers) test your changes? Is there anything specific they need to know?
    cargo run --features free_camera --example ssao (modified)
    The example uses a finite perspective projection with a large near/far range to make the artifact clearly visible. Bevy's default perspective projection uses an infinite far plane, which causes the relevant shader values to approach infinity and effectively hides this particular artifact.
  • If relevant, what platforms did you test these changes on, and are there any important ones you can't test? Not relevant

Showcase

Before:
image

Perspective

Orthographic

After:
image

Click to view example
//! A scene showcasing screen space ambient occlusion.

use bevy::{
    camera::{CameraProjection, SubCameraView},
    camera_controller::free_camera::{FreeCamera, FreeCameraPlugin},
    pbr::ScreenSpaceAmbientOcclusion,
    prelude::*,
};

fn main() {
    App::new()
        .insert_resource(GlobalAmbientLight {
            brightness: 2000.,
            ..default()
        })
        .add_plugins((DefaultPlugins, FreeCameraPlugin))
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(2., 3., 7.).looking_at(Vec3::new(1.5, 0., 1.5), Vec3::Y),
        Msaa::Off,
        FreeCamera::default(),
        ScreenSpaceAmbientOcclusion::default(),
        // The issue is harder to reproduce with bevy's built-in perspective
        // projection because it uses an infinite far plane.
        Projection::custom(PerspectiveCustom(PerspectiveProjection {
            near: 0.01,
            far: 10000.,
            ..Default::default()
        })),
        // The artifact is easier to observe with TAA enabled and the built-in
        // orthographic projection using a large near/far range. To reproduce it,
        // uncomment both lines below and slowly resize the window with the mouse.
        // Artifacts should become visible while the aspect ratio is changing.
        // bevy::anti_alias::taa::TemporalAntiAliasing::default(),
        // Projection::Orthographic(OrthographicProjection {
        //     near: 0.01,
        //     far: 10000.,
        //     scale: 2.,
        //     scaling_mode: bevy::camera::ScalingMode::FixedVertical {
        //         viewport_height: 1.,
        //     },
        //     ..OrthographicProjection::default_3d()
        // })
    ));

    let transparent = materials.add(StandardMaterial {
        base_color: Color::srgba(0.5, 0.5, 1., 0.75),
        alpha_mode: AlphaMode::Blend,
        ..default()
    });
    let opaque = materials.add(StandardMaterial {
        base_color: Color::srgba(0.5, 1., 0.5, 1.),
        ..default()
    });

    for (xyz, mat) in [
        ([0., 0., 0.], transparent.clone()),
        ([1.5, 0., 0.], opaque.clone()),
        ([3., 0., 0.], transparent.clone()),
        ([0., 0., 1.5], opaque.clone()),
        ([1.5, 0., 1.5], transparent.clone()),
        ([3., 0., 1.5], opaque.clone()),
        ([0., 0., 3.], transparent.clone()),
        ([1.5, 0., 3.], opaque.clone()),
        ([3., 0., 3.], transparent.clone()),
    ] {
        commands.spawn((
            Mesh3d(meshes.add(Cuboid::default())),
            MeshMaterial3d(mat),
            Transform::from_xyz(xyz[0], xyz[1], xyz[2]),
        ));
    }

    commands.spawn((
        DirectionalLight {
            shadow_maps_enabled: false,
            ..default()
        },
        Transform::IDENTITY.looking_to(Dir3::NEG_X, Dir3::Y),
    ));
}

#[derive(Debug, Clone)]
struct PerspectiveCustom(PerspectiveProjection);

impl CameraProjection for PerspectiveCustom {
    fn get_clip_from_view(&self) -> Mat4 {
        let f = 1. / (self.0.fov * 0.5).tan();
        let z = self.0.near / (self.0.far - self.0.near);

        Mat4::from_cols(
            Vec4::new(f / self.0.aspect_ratio, 0., 0., 0.),
            Vec4::new(0., f, 0., 0.),
            Vec4::new(0., 0., z, -1.),
            Vec4::new(0., 0., self.0.far * z, 0.),
        )
    }

    fn get_clip_from_view_for_sub(&self, subview: &SubCameraView) -> Mat4 {
        self.0.get_clip_from_view_for_sub(subview)
    }

    fn update(&mut self, width: f32, height: f32) {
        self.0.update(width, height)
    }

    fn far(&self) -> f32 {
        self.0.far()
    }

    fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] {
        self.0.get_frustum_corners(z_near, z_far)
    }
}

Screen-space ambient occlusion may produce artifacts for meshes that do not contribute to the depth prepass. SSAO is sampled independently of whether the corresponding surface was written to the depth buffer, so regions not covered by the depth prepass may contain invalid or uninitialized data.

This primarily affects transparent meshes and other cases where depth writes are disabled.

Only enable SSAO when depth_write_enabled is true. Since depth-write behavior is derived from multiple pipeline properties, use the already resolved depth_write_enabled value rather than trying to infer it from individual material or pipeline flags.
@JMS55

JMS55 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Could you come up with a more realistic example? E.g. expand the SSAO example a bit with a flat plane, and some transparent and opaque cubes scattered around.

It's hard to tell what this would look like from your screenshot.

@venhelhardt

Copy link
Copy Markdown
Contributor Author

Could you come up with a more realistic example? E.g. expand the SSAO example a bit with a flat plane, and some transparent and opaque cubes scattered around.

It's hard to tell what this would look like from your screenshot.

Thanks for the prompt response!

I think this is actually a fairly realistic example - we do use near/far ranges like this.

More importantly, the underlying problem is independent of the particular scene. In this example, the mesh does not contribute to the depth prepass at all, so the SSAO compute path simply does not have the data it relies on. The textures themselves are initialized - for example, the reconstructed normals may end up being zero - but those values do not represent meaningful depth or surface normals for this mesh, and the SSAO algorithm is not designed to operate on such input.

With a sufficiently large near/far range, the floating-point errors involved in reconstructing the view-space data make the resulting artifact much more visible. The default infinite-far perspective projection happens to hide this particular manifestation of the problem.

More fundamentally, I think applying SSAO to a mesh that did not contribute the depth/normal information SSAO relies on is simply incorrect, regardless of whether a particular scene makes the artifact visually obvious.

I can try to expand the SSAO example with a plane and a mix of transparent and opaque cubes, but I am not quite sure what additional behavior we want that example to demonstrate. To me, the logical error here seems independent of the particular scene setup.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-Rendering Drawing game state to the screen D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 11, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Aug 11, 2026
@venhelhardt

Copy link
Copy Markdown
Contributor Author

@JMS55

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

3 participants