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.
This commit is contained in:
Patrick Walton 2024-04-15 15:37:52 -05:00 committed by GitHub
parent 09a1f94d14
commit 1141e731ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 99 additions and 26 deletions

View file

@ -462,7 +462,7 @@ impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterial
pub type RenderMaterialInstances<M> = ExtractedInstances<AssetId<M>>;
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<M: Material>(
.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<M: Material>(
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<M: Material>(
);
}
}
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<M: Material> RenderAsset for PreparedMaterial<M> {
SRes<FallbackImage>,
SRes<MaterialPipeline<M>>,
SRes<DefaultOpaqueRendererMethod>,
SRes<Msaa>,
);
fn prepare_asset(
material: Self::SourceAsset,
(render_device, images, fallback_image, pipeline, default_opaque_render_method): &mut SystemParamItem<Self::Param>,
(render_device, images, fallback_image, pipeline, default_opaque_render_method, msaa): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
match material.as_bind_group(
&pipeline.material_layout,
@ -874,7 +879,7 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
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,

View file

@ -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 05 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<StandardMaterialUniform> 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() {

View file

@ -794,8 +794,9 @@ pub fn queue_prepass_material_meshes<M: Material>(
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<M: Material>(
}
};
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<M: Material>(
);
}
}
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<M: Material>(
);
}
}
AlphaMode::Blend
| AlphaMode::Premultiplied
| AlphaMode::Add
| AlphaMode::Multiply => {}
_ => {}
}
}
}

View file

@ -1690,7 +1690,8 @@ pub fn queue_shadows<M: Material>(
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(

View file

@ -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),
})

View file

@ -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

View file

@ -30,7 +30,11 @@ fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4<f32>)
}
#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;

View file

@ -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;
}

View file

@ -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

View file

@ -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.
///

View file

@ -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<Time>, mut materials: ResMut<Assets<StandardMaterial>>) {
let alpha = (time.elapsed_seconds().sin() / 2.0) + 0.5;
for (_, material) in materials.iter_mut() {