mirror of
https://github.com/bevyengine/bevy.git
synced 2026-07-01 00:05:45 -04:00
2837e0372d
Expose the SSAO sampling radius as a public configuration option on
ScreenSpaceAmbientOcclusion.
Right now Bevy exposes SSAO quality and constant object thickness, but
the effect
radius is still hardcoded internally in the SSAO shader. That makes it
impossible for
users to tune the size of the occlusion effect from app code.
## Solution
Add a radius: f32 field to ScreenSpaceAmbientOcclusion and preserve
existing behavior
by defaulting it to the shader’s previous hardcoded value.
To wire that through cleanly, the SSAO render path now uploads a small
SSAO settings
uniform containing both radius and constant_object_thickness, and the
SSAO shader
reads settings.radius instead of a hardcoded constant.
The SSAO example was also updated to expose the new setting
interactively so the
feature is discoverable and easy to validate.
## Testing
I tested this by:
- Running cargo check -p bevy_pbr --quiet
- Building and running cargo build --example ssao
- Launching target/debug/examples/ssao and verifying the example starts
successfully
- Verifying that the new radius control can be adjusted interactively in
the example
with Left / Right
Parts that could use more testing:
- Visual validation across a wider range of scenes and camera scales
- Broader GPU/platform coverage, since I only tested locally
How reviewers can test this:
1. Run cargo build --example ssao
2. Launch target/debug/examples/ssao
3. Use Left / Right to decrease/increase SSAO radius
4. Confirm that larger values produce broader occlusion while the
default matches
existing behavior
Platform tested:
- Linux (EndeavourOS)
Platforms not tested:
- Windows
- macOS
- Web targets
217 lines
6.6 KiB
Rust
217 lines
6.6 KiB
Rust
//! A scene showcasing screen space ambient occlusion.
|
|
|
|
use bevy::{
|
|
anti_alias::taa::TemporalAntiAliasing,
|
|
camera::Hdr,
|
|
math::ops,
|
|
pbr::{ScreenSpaceAmbientOcclusion, ScreenSpaceAmbientOcclusionQualityLevel},
|
|
prelude::*,
|
|
render::camera::TemporalJitter,
|
|
};
|
|
use std::f32::consts::PI;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.insert_resource(GlobalAmbientLight {
|
|
brightness: 1000.,
|
|
..default()
|
|
})
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, update)
|
|
.run();
|
|
}
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
commands.spawn((
|
|
Camera3d::default(),
|
|
Transform::from_xyz(-2.0, 2.0, -2.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
Hdr,
|
|
Msaa::Off,
|
|
ScreenSpaceAmbientOcclusion::default(),
|
|
TemporalAntiAliasing::default(),
|
|
));
|
|
|
|
let material = materials.add(StandardMaterial {
|
|
base_color: Color::srgb(0.5, 0.5, 0.5),
|
|
perceptual_roughness: 1.0,
|
|
reflectance: 0.0,
|
|
..default()
|
|
});
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::default())),
|
|
MeshMaterial3d(material.clone()),
|
|
Transform::from_xyz(0.0, 0.0, 1.0),
|
|
));
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::default())),
|
|
MeshMaterial3d(material.clone()),
|
|
Transform::from_xyz(0.0, -1.0, 0.0),
|
|
));
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Cuboid::default())),
|
|
MeshMaterial3d(material),
|
|
Transform::from_xyz(1.0, 0.0, 0.0),
|
|
));
|
|
commands.spawn((
|
|
Mesh3d(meshes.add(Sphere::new(0.4).mesh().uv(72, 36))),
|
|
MeshMaterial3d(materials.add(StandardMaterial {
|
|
base_color: Color::srgb(0.4, 0.4, 0.4),
|
|
perceptual_roughness: 1.0,
|
|
reflectance: 0.0,
|
|
..default()
|
|
})),
|
|
SphereMarker,
|
|
));
|
|
|
|
commands.spawn((
|
|
DirectionalLight {
|
|
shadow_maps_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, PI * -0.15, PI * -0.15)),
|
|
));
|
|
|
|
commands.spawn((
|
|
Text::default(),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
bottom: px(12),
|
|
left: px(12),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
fn update(
|
|
camera: Single<
|
|
(
|
|
Entity,
|
|
Option<&ScreenSpaceAmbientOcclusion>,
|
|
Option<&TemporalJitter>,
|
|
),
|
|
With<Camera>,
|
|
>,
|
|
mut text: Single<&mut Text>,
|
|
mut sphere: Single<&mut Transform, With<SphereMarker>>,
|
|
mut commands: Commands,
|
|
keycode: Res<ButtonInput<KeyCode>>,
|
|
time: Res<Time>,
|
|
) {
|
|
sphere.translation.y = ops::sin(time.elapsed_secs() / 1.7) * 0.7;
|
|
|
|
let (camera_entity, ssao, temporal_jitter) = *camera;
|
|
let current_ssao = ssao.cloned().unwrap_or_default();
|
|
|
|
let mut commands = commands.entity(camera_entity);
|
|
commands
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Low,
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::Digit2),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Medium,
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::Digit3),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::High,
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::Digit4),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
quality_level: ScreenSpaceAmbientOcclusionQualityLevel::Ultra,
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::Digit5),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
constant_object_thickness: (current_ssao.constant_object_thickness * 2.0).min(4.0),
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::ArrowUp),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
constant_object_thickness: (current_ssao.constant_object_thickness * 0.5)
|
|
.max(0.0625),
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::ArrowDown),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
radius: (current_ssao.radius * 1.25).min(8.0),
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::ArrowRight),
|
|
)
|
|
.insert_if(
|
|
ScreenSpaceAmbientOcclusion {
|
|
radius: (current_ssao.radius * 0.8).max(0.1),
|
|
..current_ssao
|
|
},
|
|
|| keycode.just_pressed(KeyCode::ArrowLeft),
|
|
);
|
|
if keycode.just_pressed(KeyCode::Digit1) {
|
|
commands.remove::<ScreenSpaceAmbientOcclusion>();
|
|
}
|
|
if keycode.just_pressed(KeyCode::Space) {
|
|
if temporal_jitter.is_some() {
|
|
commands.remove::<TemporalJitter>();
|
|
} else {
|
|
commands.insert(TemporalJitter::default());
|
|
}
|
|
}
|
|
|
|
text.clear();
|
|
|
|
let (o, l, m, h, u) = match ssao.map(|s| s.quality_level) {
|
|
None => ("*", "", "", "", ""),
|
|
Some(ScreenSpaceAmbientOcclusionQualityLevel::Low) => ("", "*", "", "", ""),
|
|
Some(ScreenSpaceAmbientOcclusionQualityLevel::Medium) => ("", "", "*", "", ""),
|
|
Some(ScreenSpaceAmbientOcclusionQualityLevel::High) => ("", "", "", "*", ""),
|
|
Some(ScreenSpaceAmbientOcclusionQualityLevel::Ultra) => ("", "", "", "", "*"),
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
if let Some(radius) = ssao.map(|s| s.radius) {
|
|
text.push_str(&format!("Radius: {radius} (Left/Right)\n"));
|
|
}
|
|
|
|
if let Some(thickness) = ssao.map(|s| s.constant_object_thickness) {
|
|
text.push_str(&format!(
|
|
"Constant object thickness: {thickness} (Up/Down)\n\n"
|
|
));
|
|
}
|
|
|
|
text.push_str("SSAO Quality:\n");
|
|
text.push_str(&format!("(1) {o}Off{o}\n"));
|
|
text.push_str(&format!("(2) {l}Low{l}\n"));
|
|
text.push_str(&format!("(3) {m}Medium{m}\n"));
|
|
text.push_str(&format!("(4) {h}High{h}\n"));
|
|
text.push_str(&format!("(5) {u}Ultra{u}\n\n"));
|
|
|
|
text.push_str("Temporal Antialiasing:\n");
|
|
text.push_str(match temporal_jitter {
|
|
Some(_) => "(Space) Enabled",
|
|
None => "(Space) Disabled",
|
|
});
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct SphereMarker;
|