mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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:
parent
09a1f94d14
commit
1141e731ff
11 changed files with 99 additions and 26 deletions
|
@ -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,
|
||||
|
|
|
@ -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<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() {
|
||||
|
|
|
@ -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 => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue