From 1141e731ff19b74a05a6290f71cdb2a0a3b74b8d Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 15 Apr 2024 15:37:52 -0500 Subject: [PATCH] Implement alpha to coverage (A2C) support. (#12970) [Alpha to coverage] (A2C) replaces alpha blending with a hardware-specific multisample coverage mask when multisample antialiasing is in use. It's a simple form of [order-independent transparency] that relies on MSAA. ["Anti-aliased Alpha Test: The Esoteric Alpha To Coverage"] is a good summary of the motivation for and best practices relating to A2C. This commit implements alpha to coverage support as a new variant for `AlphaMode`. You can supply `AlphaMode::AlphaToCoverage` as the `alpha_mode` field in `StandardMaterial` to use it. When in use, the standard material shader automatically applies the texture filtering method from ["Anti-aliased Alpha Test: The Esoteric Alpha To Coverage"]. Objects with alpha-to-coverage materials are binned in the opaque pass, as they're fully order-independent. The `transparency_3d` example has been updated to feature an object with alpha to coverage. Happily, the example was already using MSAA. This is part of #2223, as far as I can tell. [Alpha to coverage]: https://en.wikipedia.org/wiki/Alpha_to_coverage [order-independent transparency]: https://en.wikipedia.org/wiki/Order-independent_transparency ["Anti-aliased Alpha Test: The Esoteric Alpha To Coverage"]: https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f --- ## Changelog ### Added * The `AlphaMode` enum now supports `AlphaToCoverage`, to provide limited order-independent transparency when multisample antialiasing is in use. --- crates/bevy_pbr/src/material.rs | 25 +++++++++++-------- crates/bevy_pbr/src/pbr_material.rs | 4 +++ crates/bevy_pbr/src/prepass/mod.rs | 19 +++++++------- crates/bevy_pbr/src/render/light.rs | 3 ++- crates/bevy_pbr/src/render/mesh.rs | 18 ++++++++++--- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 14 +++++++++++ crates/bevy_pbr/src/render/pbr_functions.wgsl | 6 ++++- .../src/render/pbr_prepass_functions.wgsl | 6 +++-- crates/bevy_pbr/src/render/pbr_types.wgsl | 1 + crates/bevy_render/src/alpha.rs | 12 +++++++++ examples/3d/transparency_3d.rs | 17 +++++++++++++ 11 files changed, 99 insertions(+), 26 deletions(-) diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index f80815c88f..690442e4fe 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -462,7 +462,7 @@ impl RenderCommand

for SetMaterial pub type RenderMaterialInstances = ExtractedInstances>; -pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> MeshPipelineKey { +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey { match alpha_mode { // Premultiplied and Add share the same pipeline key // They're made distinct in the PBR shader, via `premultiply_alpha()` @@ -470,6 +470,10 @@ pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> MeshPipelineKey { AlphaMode::Blend => MeshPipelineKey::BLEND_ALPHA, AlphaMode::Multiply => MeshPipelineKey::BLEND_MULTIPLY, AlphaMode::Mask(_) => MeshPipelineKey::MAY_DISCARD, + AlphaMode::AlphaToCoverage => match *msaa { + Msaa::Off => MeshPipelineKey::MAY_DISCARD, + _ => MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE, + }, _ => MeshPipelineKey::NONE, } } @@ -693,8 +697,10 @@ pub fn queue_material_meshes( .material_bind_group_id .set(material.get_bind_group_id()); - match material.properties.alpha_mode { - AlphaMode::Opaque => { + match mesh_key + .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) + { + MeshPipelineKey::BLEND_OPAQUE | MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE => { if material.properties.reads_view_transmission_texture { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; @@ -717,7 +723,8 @@ pub fn queue_material_meshes( opaque_phase.add(bin_key, *visible_entity, mesh_instance.should_batch()); } } - AlphaMode::Mask(_) => { + // Alpha mask + MeshPipelineKey::MAY_DISCARD => { if material.properties.reads_view_transmission_texture { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; @@ -743,10 +750,7 @@ pub fn queue_material_meshes( ); } } - AlphaMode::Blend - | AlphaMode::Premultiplied - | AlphaMode::Add - | AlphaMode::Multiply => { + _ => { let distance = rangefinder.distance_translation(&mesh_instance.translation) + material.properties.depth_bias; transparent_phase.add(Transparent3d { @@ -851,11 +855,12 @@ impl RenderAsset for PreparedMaterial { SRes, SRes>, SRes, + SRes, ); fn prepare_asset( material: Self::SourceAsset, - (render_device, images, fallback_image, pipeline, default_opaque_render_method): &mut SystemParamItem, + (render_device, images, fallback_image, pipeline, default_opaque_render_method, msaa): &mut SystemParamItem, ) -> Result> { match material.as_bind_group( &pipeline.material_layout, @@ -874,7 +879,7 @@ impl RenderAsset for PreparedMaterial { MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, material.reads_view_transmission_texture(), ); - mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode(), msaa)); Ok(PreparedMaterial { bindings: prepared.bindings, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index d422028785..f19aef98df 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -648,6 +648,7 @@ bitflags::bitflags! { const ALPHA_MODE_PREMULTIPLIED = 3 << Self::ALPHA_MODE_SHIFT_BITS; // const ALPHA_MODE_ADD = 4 << Self::ALPHA_MODE_SHIFT_BITS; // Right now only values 0–5 are used, which still gives const ALPHA_MODE_MULTIPLY = 5 << Self::ALPHA_MODE_SHIFT_BITS; // ← us "room" for two more modes without adding more bits + const ALPHA_MODE_ALPHA_TO_COVERAGE = 6 << Self::ALPHA_MODE_SHIFT_BITS; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -783,6 +784,9 @@ impl AsBindGroupShaderType for StandardMaterial { AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED, AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD, AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, + AlphaMode::AlphaToCoverage => { + flags |= StandardMaterialFlags::ALPHA_MODE_ALPHA_TO_COVERAGE; + } }; if self.attenuation_distance.is_finite() { diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index fea9817553..682935c504 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -794,8 +794,9 @@ pub fn queue_prepass_material_meshes( let alpha_mode = material.properties.alpha_mode; match alpha_mode { - AlphaMode::Opaque => {} - AlphaMode::Mask(_) => mesh_key |= MeshPipelineKey::MAY_DISCARD, + AlphaMode::Opaque | AlphaMode::AlphaToCoverage | AlphaMode::Mask(_) => { + mesh_key |= alpha_mode_pipeline_key(alpha_mode, &msaa); + } AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add @@ -849,8 +850,10 @@ pub fn queue_prepass_material_meshes( } }; - match alpha_mode { - AlphaMode::Opaque => { + match mesh_key + .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) + { + MeshPipelineKey::BLEND_OPAQUE | MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE => { if deferred { opaque_deferred_phase.as_mut().unwrap().add( OpaqueNoLightmap3dBinKey { @@ -875,7 +878,8 @@ pub fn queue_prepass_material_meshes( ); } } - AlphaMode::Mask(_) => { + // Alpha mask + MeshPipelineKey::MAY_DISCARD => { if deferred { let bin_key = OpaqueNoLightmap3dBinKey { pipeline: pipeline_id, @@ -902,10 +906,7 @@ pub fn queue_prepass_material_meshes( ); } } - AlphaMode::Blend - | AlphaMode::Premultiplied - | AlphaMode::Add - | AlphaMode::Multiply => {} + _ => {} } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 9a964d2c27..e09b162197 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1690,7 +1690,8 @@ pub fn queue_shadows( AlphaMode::Mask(_) | AlphaMode::Blend | AlphaMode::Premultiplied - | AlphaMode::Add => MeshPipelineKey::MAY_DISCARD, + | AlphaMode::Add + | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, _ => MeshPipelineKey::NONE, }; let pipeline_id = pipelines.specialize( diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 35e75b9341..93b7ed9918 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1063,6 +1063,7 @@ bitflags::bitflags! { const BLEND_PREMULTIPLIED_ALPHA = 1 << Self::BLEND_SHIFT_BITS; // const BLEND_MULTIPLY = 2 << Self::BLEND_SHIFT_BITS; // ← We still have room for one more value without adding more bits const BLEND_ALPHA = 3 << Self::BLEND_SHIFT_BITS; + const BLEND_ALPHA_TO_COVERAGE = 4 << Self::BLEND_SHIFT_BITS; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -1101,7 +1102,7 @@ impl MeshPipelineKey { const MSAA_MASK_BITS: u32 = 0b111; const MSAA_SHIFT_BITS: u32 = Self::LAST_FLAG.bits().trailing_zeros() + 1; - const BLEND_MASK_BITS: u32 = 0b11; + const BLEND_MASK_BITS: u32 = 0b111; const BLEND_SHIFT_BITS: u32 = Self::MSAA_MASK_BITS.count_ones() + Self::MSAA_SHIFT_BITS; const TONEMAP_METHOD_MASK_BITS: u32 = 0b111; @@ -1278,7 +1279,7 @@ impl SpecializedMeshPipeline for MeshPipeline { let (label, blend, depth_write_enabled); let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); - let mut is_opaque = false; + let (mut is_opaque, mut alpha_to_coverage_enabled) = (false, false); if pass == MeshPipelineKey::BLEND_ALPHA { label = "alpha_blend_mesh_pipeline".into(); blend = Some(BlendState::ALPHA_BLENDING); @@ -1308,6 +1309,17 @@ impl SpecializedMeshPipeline for MeshPipeline { // For the multiply pass, fragments that are closer will be alpha blended // but their depth is not written to the depth buffer depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE { + label = "alpha_to_coverage_mesh_pipeline".into(); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + alpha_to_coverage_enabled = true; + shader_defs.push("ALPHA_TO_COVERAGE".into()); } else { label = "opaque_mesh_pipeline".into(); // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases @@ -1507,7 +1519,7 @@ impl SpecializedMeshPipeline for MeshPipeline { multisample: MultisampleState { count: key.msaa_samples(), mask: !0, - alpha_to_coverage_enabled: false, + alpha_to_coverage_enabled, }, label: Some(label), }) diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index c3b3e94988..d64e78b47e 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -110,6 +110,20 @@ fn pbr_input_from_standard_material( #else pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias); #endif + +#ifdef ALPHA_TO_COVERAGE + // Sharpen alpha edges. + // + // https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f + let alpha_mode = pbr_bindings::material.flags & + pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { + pbr_input.material.base_color.a = (pbr_input.material.base_color.a - + pbr_bindings::material.alpha_cutoff) / + max(fwidth(pbr_input.material.base_color.a), 0.0001) + 0.5; + } +#endif // ALPHA_TO_COVERAGE + } #endif // VERTEX_UVS diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index bd1173f2b5..51a914ced4 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -30,7 +30,11 @@ fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) } #ifdef MAY_DISCARD - else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { + // NOTE: `MAY_DISCARD` is only defined in the alpha to coverage case if MSAA + // was off. This special situation causes alpha to coverage to fall back to + // alpha mask. + else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { if color.a >= material.alpha_cutoff { // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque color.a = 1.0; diff --git a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl index aab3a2abfb..4fcaa33e89 100644 --- a/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_prepass_functions.wgsl @@ -8,7 +8,7 @@ pbr_types, } -// Cutoff used for the premultiplied alpha modes BLEND and ADD. +// Cutoff used for the premultiplied alpha modes BLEND, ADD, and ALPHA_TO_COVERAGE. const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; // We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff @@ -30,7 +30,9 @@ fn prepass_alpha_discard(in: VertexOutput) { if output_color.a < pbr_bindings::material.alpha_cutoff { discard; } - } else if (alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD) { + } else if (alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE) { if output_color.a < PREMULTIPLIED_ALPHA_CUTOFF { discard; } diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 5ecc6e893e..98d0e56918 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -51,6 +51,7 @@ const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 1073741824u; const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 1610612736u; // (3u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 2147483648u; // (4u32 << 29) const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 2684354560u; // (5u32 << 29) +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE: u32 = 3221225472u; // (6u32 << 29) // ↑ To calculate/verify the values above, use the following playground: // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7792f8dd6fc6a8d4d0b6b1776898a7f4 diff --git a/crates/bevy_render/src/alpha.rs b/crates/bevy_render/src/alpha.rs index 2dfad77ac9..13a4943c10 100644 --- a/crates/bevy_render/src/alpha.rs +++ b/crates/bevy_render/src/alpha.rs @@ -32,6 +32,18 @@ pub enum AlphaMode { /// Can be used to avoid “border” or “outline” artifacts that can occur /// when using plain alpha-blended textures. Premultiplied, + /// Spreads the fragment out over a hardware-dependent number of sample + /// locations proportional to the alpha value. This requires multisample + /// antialiasing; if MSAA isn't on, this is identical to + /// [`AlphaMode::Mask`] with a value of 0.5. + /// + /// Alpha to coverage provides improved performance and better visual + /// fidelity over [`AlphaMode::Blend`], as Bevy doesn't have to sort objects + /// when it's in use. It's especially useful for complex transparent objects + /// like foliage. + /// + /// [alpha to coverage]: https://en.wikipedia.org/wiki/Alpha_to_coverage + AlphaToCoverage, /// Combines the color of the fragments with the colors behind them in an /// additive process, (i.e. like light) producing lighter results. /// diff --git a/examples/3d/transparency_3d.rs b/examples/3d/transparency_3d.rs index 7383ebabd1..63a9c43210 100644 --- a/examples/3d/transparency_3d.rs +++ b/examples/3d/transparency_3d.rs @@ -67,6 +67,18 @@ fn setup( ..default() }); + // Transparent cube, uses `alpha_mode: AlphaToCoverage` + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::default()), + material: materials.add(StandardMaterial { + base_color: Color::srgba(0.5, 1.0, 0.5, 0.0), + alpha_mode: AlphaMode::AlphaToCoverage, + ..default() + }), + transform: Transform::from_xyz(-1.5, 0.5, 0.0), + ..default() + }); + // Opaque sphere commands.spawn(PbrBundle { mesh: meshes.add(Sphere::new(0.5).mesh().ico(3).unwrap()), @@ -98,6 +110,11 @@ fn setup( /// - [`Mask(f32)`](AlphaMode::Mask): Object appears when the alpha value goes above the mask's threshold, disappears /// when the alpha value goes back below the threshold. /// - [`Blend`](AlphaMode::Blend): Object fades in and out smoothly. +/// - [`AlphaToCoverage`](AlphaMode::AlphaToCoverage): Object fades in and out +/// in steps corresponding to the number of multisample antialiasing (MSAA) +/// samples in use. For example, assuming 8xMSAA, the object will be +/// completely opaque, then will be 7/8 opaque (1/8 transparent), then will be +/// 6/8 opaque, then 5/8, etc. pub fn fade_transparency(time: Res