From dd86d82323294a0c3d9be9008e1175e8fe2e8027 Mon Sep 17 00:00:00 2001 From: altunenes Date: Mon, 25 May 2026 00:05:39 +0300 Subject: [PATCH 1/6] Add geometric specular aa fields to StandardMaterial --- crates/bevy_pbr/src/pbr_material.rs | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index ed69dc0979acc..da13c888d220d 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -672,6 +672,36 @@ pub struct StandardMaterial { /// Whether to enable fog for this material. pub fog_enabled: bool, + /// Whether to enable geometric specular aa. + /// + /// When enabled, the PBR shader widens the roughness based on the + /// screen space variance of the shading normal, suppressing the + /// shimmering specular "fireflies" that high freq normal maps and + /// sharp geometric curvature produce at low roughness... + /// + /// Based on Tokuyoshi and Kaplanyan, "Improved Geometric Specular + /// Antialiasing" (I3D 2019) paper: https://www.jp.square-enix.com/tech/library/pdf/ImprovedGeometricSpecularAA.pdf + /// + /// Defaults to `false`. + pub specular_anti_aliasing: bool, + + /// Strength of [`StandardMaterial::specular_anti_aliasing`], the + /// screen-space variance σ of the filtering kernel. Higher values + /// widen the roughness more aggressively. + /// + /// Only has an effect when [`StandardMaterial::specular_anti_aliasing`] + /// is `true`. Defaults to `0.5`. + pub specular_anti_aliasing_screen_space_variance: f32, + + /// Clamping threshold κ for [`StandardMaterial::specular_anti_aliasing`]. + /// Caps the maximum amount of squared roughness the filter is allowed + /// to add, preventing over-filtering where screen-space derivatives + /// spike. + /// + /// Only has an effect when [`StandardMaterial::specular_anti_aliasing`] + /// is `true`. Defaults to `0.18`. + pub specular_anti_aliasing_threshold: f32, + /// How to apply the alpha channel of the `base_color_texture`. /// /// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`]. @@ -926,6 +956,9 @@ impl Default for StandardMaterial { cull_mode: Some(Face::Back), unlit: false, fog_enabled: true, + specular_anti_aliasing: false, + specular_anti_aliasing_screen_space_variance: 0.5, + specular_anti_aliasing_threshold: 0.18, alpha_mode: AlphaMode::Opaque, depth_bias: 0.0, depth_map: None, @@ -989,6 +1022,7 @@ bitflags::bitflags! { const ANISOTROPY_TEXTURE = 1 << 17; const SPECULAR_TEXTURE = 1 << 18; const SPECULAR_TINT_TEXTURE = 1 << 19; + const SPECULAR_ANTI_ALIASING = 1 << 20; const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. @@ -1061,6 +1095,10 @@ pub struct StandardMaterialUniform { pub max_relief_mapping_search_steps: u32, /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. pub deferred_lighting_pass_id: u32, + /// Screen-space variance (σ) for geometric specular anti-aliasing. + pub specular_anti_aliasing_screen_space_variance: f32, + /// Clamping threshold (κ) for geometric specular anti-aliasing. + pub specular_anti_aliasing_threshold: f32, } impl AsBindGroupShaderType for StandardMaterial { @@ -1090,6 +1128,9 @@ impl AsBindGroupShaderType for StandardMaterial { if self.fog_enabled { flags |= StandardMaterialFlags::FOG_ENABLED; } + if self.specular_anti_aliasing { + flags |= StandardMaterialFlags::SPECULAR_ANTI_ALIASING; + } if self.depth_map.is_some() { flags |= StandardMaterialFlags::DEPTH_MAP; } @@ -1208,6 +1249,9 @@ impl AsBindGroupShaderType for StandardMaterial { max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, uv_transform: self.uv_transform.into(), + specular_anti_aliasing_screen_space_variance: self + .specular_anti_aliasing_screen_space_variance, + specular_anti_aliasing_threshold: self.specular_anti_aliasing_threshold, } } } From 359c1b8df51ad691ca2f563cb195ae54178c9561 Mon Sep 17 00:00:00 2001 From: altunenes Date: Mon, 25 May 2026 00:11:21 +0300 Subject: [PATCH 2/6] apply specular aa kernel to roughness in PBR --- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index b09131c7be1eb..5647916a943d3 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -838,6 +838,44 @@ pbr_input.material.uv_transform = uv_transform; pbr_input.lightmap_light = lightmap(in.uv_b, lightmap_exposure, in.instance_index); #endif + + // Geometric specular aa. + // Tokuyoshi and Kaplanyan, "Improved Geometric Specular Antialiasing" (I3D 2019). +#ifndef MESHLET_MESH_MATERIAL_PASS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_ANTI_ALIASING_BIT) != 0u) { +#ifdef BINDLESS + let sigma = pbr_bindings::material_array[material_indices[slot].material].specular_anti_aliasing_screen_space_variance; + let kappa = pbr_bindings::material_array[material_indices[slot].material].specular_anti_aliasing_threshold; +#else // BINDLESS + let sigma = pbr_bindings::material.specular_anti_aliasing_screen_space_variance; + let kappa = pbr_bindings::material.specular_anti_aliasing_threshold; +#endif // BINDLESS + let sigma2 = sigma * sigma; + + let dndx = dpdx(pbr_input.N); + let dndy = dpdy(pbr_input.N); + let variance = sigma2 * (dot(dndx, dndx) + dot(dndy, dndy)); + let kernel_roughness = min(variance, kappa); + + // perceptual_roughness^2 is the GGX alpha. Widen alpha^2 by the kernel and convert back. + let alpha = pbr_input.material.perceptual_roughness * + pbr_input.material.perceptual_roughness; + pbr_input.material.perceptual_roughness = + sqrt(sqrt(saturate(alpha * alpha + kernel_roughness))); + +#ifdef STANDARD_MATERIAL_CLEARCOAT + let dcdx = dpdx(pbr_input.clearcoat_N); + let dcdy = dpdy(pbr_input.clearcoat_N); + let cc_variance = sigma2 * (dot(dcdx, dcdx) + dot(dcdy, dcdy)); + let cc_kernel = min(cc_variance, kappa); + + let cc_alpha = pbr_input.material.clearcoat_perceptual_roughness * + pbr_input.material.clearcoat_perceptual_roughness; + pbr_input.material.clearcoat_perceptual_roughness = + sqrt(sqrt(saturate(cc_alpha * cc_alpha + cc_kernel))); +#endif // STANDARD_MATERIAL_CLEARCOAT + } +#endif // MESHLET_MESH_MATERIAL_PASS } return pbr_input; From 610d9c1f80ab603d1a0df4136bbee805f3d3bb15 Mon Sep 17 00:00:00 2001 From: altunenes Date: Mon, 25 May 2026 00:12:52 +0300 Subject: [PATCH 3/6] Expose specular anti aliasing param. default vals threshold 0.18 & variance 0.5 come from paper. --- crates/bevy_pbr/src/render/pbr_types.wgsl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index b8b51c577ecef..64c4aeae4f925 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -28,6 +28,8 @@ struct StandardMaterial { max_relief_mapping_search_steps: u32, /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. deferred_lighting_pass_id: u32, + specular_anti_aliasing_screen_space_variance: f32, + specular_anti_aliasing_threshold: f32, }; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -54,6 +56,7 @@ const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 1u << 16u const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 1u << 17u; const STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT: u32 = 1u << 18u; const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 1u << 19u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_ANTI_ALIASING_BIT: u32 = 1u << 20u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 7u << 29u; // (0b111u << 29u) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u << 29u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1u << 29u; @@ -88,6 +91,8 @@ fn standard_material_new() -> StandardMaterial { material.max_parallax_layer_count = 16.0; material.max_relief_mapping_search_steps = 5u; material.deferred_lighting_pass_id = 1u; + material.specular_anti_aliasing_screen_space_variance = 0.5; + material.specular_anti_aliasing_threshold = 0.18; // scale 1, translation 0, rotation 0 material.uv_transform = mat3x3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); From e780495a416993c7fe75d8ee0e27818ce43cd54a Mon Sep 17 00:00:00 2001 From: altunenes Date: Mon, 25 May 2026 03:07:01 +0300 Subject: [PATCH 4/6] clipy fix --- crates/bevy_pbr/src/pbr_material.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index da13c888d220d..a71466380ff29 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -680,7 +680,8 @@ pub struct StandardMaterial { /// sharp geometric curvature produce at low roughness... /// /// Based on Tokuyoshi and Kaplanyan, "Improved Geometric Specular - /// Antialiasing" (I3D 2019) paper: https://www.jp.square-enix.com/tech/library/pdf/ImprovedGeometricSpecularAA.pdf + /// Antialiasing" (I3D 2019): + /// . /// /// Defaults to `false`. pub specular_anti_aliasing: bool, From 6e7f79c622cf3a79719c03a75a4d6f6634ce3ee2 Mon Sep 17 00:00:00 2001 From: altunenes Date: Tue, 26 May 2026 14:11:19 +0300 Subject: [PATCH 5/6] add specular aa example --- Cargo.toml | 11 + examples/3d/specular_anti_aliasing.rs | 460 ++++++++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 examples/3d/specular_anti_aliasing.rs diff --git a/Cargo.toml b/Cargo.toml index 4f032612a2dee..a386bdae9a145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1256,6 +1256,17 @@ category = "3D Rendering" # TAA not supported by WebGL wasm = false +[[example]] +name = "specular_anti_aliasing" +path = "examples/3d/specular_anti_aliasing.rs" +doc-scrape-examples = true + +[package.metadata.example.specular_anti_aliasing] +name = "Specular Anti-aliasing" +description = "Demonstrates the geometric specular anti-aliasing toggle on StandardMaterial" +category = "3D Rendering" +wasm = false + [[example]] name = "atmospheric_fog" path = "examples/3d/atmospheric_fog.rs" diff --git a/examples/3d/specular_anti_aliasing.rs b/examples/3d/specular_anti_aliasing.rs new file mode 100644 index 0000000000000..e6439b03c76e2 --- /dev/null +++ b/examples/3d/specular_anti_aliasing.rs @@ -0,0 +1,460 @@ +//! Demonstrates the `specular_anti_aliasing` toggle on [`StandardMaterial`]. +//! +//! A normal-mapped cube rotates under an orbiting point light. Press Space +//! to toggle specular anti-aliasing; roughness, variance, threshold and +//! light intensity can also be adjusted live with the keyboard. + +use bevy::{ + anti_alias::taa::TemporalAntiAliasing, + asset::RenderAssetUsages, + camera::Hdr, + color::palettes::css::WHITE, + core_pipeline::{ + prepass::{DepthPrepass, MotionVectorPrepass}, + tonemapping::Tonemapping::AcesFitted, + }, + image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}, + input::mouse::{MouseMotion, MouseWheel}, + light::Skybox, + math::{ops, vec3, Affine2}, + post_process::bloom::Bloom, + prelude::*, + render::{ + camera::{MipBias, TemporalJitter}, + render_resource::{Extent3d, TextureDimension, TextureFormat}, + }, +}; + +type TaaComponents = ( + TemporalAntiAliasing, + TemporalJitter, + MipBias, + DepthPrepass, + MotionVectorPrepass, +); + +const CAMERA_MIN_DISTANCE: f32 = 1.5; +const CAMERA_MAX_DISTANCE: f32 = 12.0; +const CAMERA_INITIAL_DISTANCE: f32 = 5.0; + +#[derive(Resource)] +struct Settings { + aa: bool, + taa: bool, + roughness: f32, + sigma: f32, + kappa: f32, + light_intensity: f32, + paused: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + aa: true, + taa: false, + roughness: 0.06, + sigma: 0.5, + kappa: 0.18, + light_intensity: 1_000_000.0, + paused: false, + } + } +} + +#[derive(Component)] +struct Subject; + +#[derive(Component)] +struct OrbitLight { + angle: f32, +} + +fn main() { + App::new() + .insert_resource(ClearColor(Color::srgb(0.05, 0.06, 0.08))) + .init_resource::() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + handle_input, + apply_settings, + apply_taa, + spin, + orbit_light, + drag_rotate, + wheel_zoom, + ), + ) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + asset_server: Res, + settings: Res, +) { + let cube = meshes.add( + Mesh::from(Cuboid::new(1.5, 1.5, 1.5)) + .with_generated_tangents() + .expect("Failed to generate tangents"), + ); + + let normal_map = images.add(build_voronoi_normal_map(512, 72)); + + commands.spawn(( + Mesh3d(cube), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb(0.55, 0.57, 0.6), + normal_map_texture: Some(normal_map), + metallic: 1.0, + reflectance: 1.0, + uv_transform: Affine2::from_scale(Vec2::splat(2.5)), + perceptual_roughness: settings.roughness, + specular_anti_aliasing: settings.aa, + specular_anti_aliasing_screen_space_variance: settings.sigma, + specular_anti_aliasing_threshold: settings.kappa, + ..default() + })), + Transform::default(), + Subject, + )); + + commands.spawn(( + PointLight { + color: WHITE.into(), + intensity: settings.light_intensity, + range: 50.0, + ..default() + }, + Transform::from_xyz(0.0, 2.0, 2.5), + OrbitLight { angle: 0.0 }, + )); + + let env_specular: Handle = + asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"); + let env_diffuse: Handle = + asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"); + + commands.spawn(( + Camera3d::default(), + Hdr, + Msaa::Sample4, + Transform::from_xyz(0.0, 0.5, CAMERA_INITIAL_DISTANCE).looking_at(Vec3::ZERO, Vec3::Y), + AcesFitted, + Bloom::NATURAL, + EnvironmentMapLight { + diffuse_map: env_diffuse, + specular_map: env_specular.clone(), + intensity: 400.0, + ..default() + }, + Skybox { + image: Some(env_specular), + brightness: 150.0, + ..default() + }, + )); + + commands.spawn(( + help_text(&settings), + TextFont { + font_size: FontSize::Px(13.0), + ..default() + }, + Node { + position_type: PositionType::Absolute, + bottom: px(10), + left: px(10), + ..default() + }, + )); +} + +fn handle_input( + keyboard: Res>, + time: Res