Skip to content

Commit 603cb43

Browse files
committed
Standard Material Blend Modes (#6644)
# Objective - This PR adds support for blend modes to the PBR `StandardMaterial`. <img width="1392" alt="Screenshot 2022-11-18 at 20 00 56" src="https://user-images.githubusercontent.com/418473/202820627-0636219a-a1e5-437a-b08b-b08c6856bf9c.png"> <img width="1392" alt="Screenshot 2022-11-18 at 20 01 01" src="https://user-images.githubusercontent.com/418473/202820615-c8d43301-9a57-49c4-bd21-4ae343c3e9ec.png"> ## Solution - The existing `AlphaMode` enum is extended, adding three more modes: `AlphaMode::Premultiplied`, `AlphaMode::Add` and `AlphaMode::Multiply`; - All new modes are rendered in the existing `Transparent3d` phase; - The existing mesh flags for alpha mode are reorganized for a more compact/efficient representation, and new values are added; - `MeshPipelineKey::TRANSPARENT_MAIN_PASS` is refactored into `MeshPipelineKey::BLEND_BITS`. - `AlphaMode::Opaque` and `AlphaMode::Mask(f32)` share a single opaque pipeline key: `MeshPipelineKey::BLEND_OPAQUE`; - `Blend`, `Premultiplied` and `Add` share a single premultiplied alpha pipeline key, `MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA`. In the shader, color values are premultiplied accordingly (or not) depending on the blend mode to produce the three different results after PBR/tone mapping/dithering; - `Multiply` uses its own independent pipeline key, `MeshPipelineKey::BLEND_MULTIPLY`; - Example and documentation are provided. --- ## Changelog ### Added - Added support for additive and multiplicative blend modes in the PBR `StandardMaterial`, via `AlphaMode::Add` and `AlphaMode::Multiply`; - Added support for premultiplied alpha in the PBR `StandardMaterial`, via `AlphaMode::Premultiplied`;
1 parent ff5e4fd commit 603cb43

File tree

11 files changed

+566
-28
lines changed

11 files changed

+566
-28
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,16 @@ description = "A scene showcasing the built-in 3D shapes"
297297
category = "3D Rendering"
298298
wasm = true
299299

300+
[[example]]
301+
name = "blend_modes"
302+
path = "examples/3d/blend_modes.rs"
303+
304+
[package.metadata.example.blend_modes]
305+
name = "Blend Modes"
306+
description = "Showcases different blend modes"
307+
category = "3D Rendering"
308+
wasm = true
309+
300310
[[example]]
301311
name = "lighting"
302312
path = "examples/3d/lighting.rs"

crates/bevy_pbr/src/alpha.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ pub enum AlphaMode {
2323
/// Standard alpha-blending is used to blend the fragment's color
2424
/// with the color behind it.
2525
Blend,
26+
/// Similar to [`AlphaMode::Blend`], however assumes RGB channel values are
27+
/// [premultiplied](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied).
28+
///
29+
/// For otherwise constant RGB values, behaves more like [`AlphaMode::Blend`] for
30+
/// alpha values closer to 1.0, and more like [`AlphaMode::Add`] for
31+
/// alpha values closer to 0.0.
32+
///
33+
/// Can be used to avoid “border” or “outline” artifacts that can occur
34+
/// when using plain alpha-blended textures.
35+
Premultiplied,
36+
/// Combines the color of the fragments with the colors behind them in an
37+
/// additive process, (i.e. like light) producing lighter results.
38+
///
39+
/// Black produces no effect. Alpha values can be used to modulate the result.
40+
///
41+
/// Useful for effects like holograms, ghosts, lasers and other energy beams.
42+
Add,
43+
/// Combines the color of the fragments with the colors behind them in a
44+
/// multiplicative process, (i.e. like pigments) producing darker results.
45+
///
46+
/// White produces no effect. Alpha values can be used to modulate the result.
47+
///
48+
/// Useful for effects like stained glass, window tint film and some colored liquids.
49+
Multiply,
2650
}
2751

2852
impl Eq for AlphaMode {}

crates/bevy_pbr/src/material.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -415,8 +415,14 @@ pub fn queue_material_meshes<M: Material>(
415415
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
416416
| view_key;
417417
let alpha_mode = material.properties.alpha_mode;
418-
if let AlphaMode::Blend = alpha_mode {
419-
mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS;
418+
if let AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add =
419+
alpha_mode
420+
{
421+
// Blend, Premultiplied and Add all share the same pipeline key
422+
// They're made distinct in the PBR shader, via `premultiply_alpha()`
423+
mesh_key |= MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA;
424+
} else if let AlphaMode::Multiply = alpha_mode {
425+
mesh_key |= MeshPipelineKey::BLEND_MULTIPLY;
420426
}
421427

422428
let pipeline_id = pipelines.specialize(
@@ -455,7 +461,10 @@ pub fn queue_material_meshes<M: Material>(
455461
distance,
456462
});
457463
}
458-
AlphaMode::Blend => {
464+
AlphaMode::Blend
465+
| AlphaMode::Premultiplied
466+
| AlphaMode::Add
467+
| AlphaMode::Multiply => {
459468
transparent_phase.add(Transparent3d {
460469
entity: *visible_entity,
461470
draw_function: draw_transparent_pbr,

crates/bevy_pbr/src/pbr_material.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,16 +298,25 @@ bitflags::bitflags! {
298298
const OCCLUSION_TEXTURE = (1 << 3);
299299
const DOUBLE_SIDED = (1 << 4);
300300
const UNLIT = (1 << 5);
301-
const ALPHA_MODE_OPAQUE = (1 << 6);
302-
const ALPHA_MODE_MASK = (1 << 7);
303-
const ALPHA_MODE_BLEND = (1 << 8);
304-
const TWO_COMPONENT_NORMAL_MAP = (1 << 9);
305-
const FLIP_NORMAL_MAP_Y = (1 << 10);
301+
const TWO_COMPONENT_NORMAL_MAP = (1 << 6);
302+
const FLIP_NORMAL_MAP_Y = (1 << 7);
303+
const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode`
304+
const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into
305+
const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7.
306+
const ALPHA_MODE_BLEND = (2 << Self::ALPHA_MODE_SHIFT_BITS); //
307+
const ALPHA_MODE_PREMULTIPLIED = (3 << Self::ALPHA_MODE_SHIFT_BITS); //
308+
const ALPHA_MODE_ADD = (4 << Self::ALPHA_MODE_SHIFT_BITS); // Right now only values 0–5 are used, which still gives
309+
const ALPHA_MODE_MULTIPLY = (5 << Self::ALPHA_MODE_SHIFT_BITS); // ← us "room" for two more modes without adding more bits
306310
const NONE = 0;
307311
const UNINITIALIZED = 0xFFFF;
308312
}
309313
}
310314

315+
impl StandardMaterialFlags {
316+
const ALPHA_MODE_MASK_BITS: u32 = 0b111;
317+
const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones();
318+
}
319+
311320
/// The GPU representation of the uniform data of a [`StandardMaterial`].
312321
#[derive(Clone, Default, ShaderType)]
313322
pub struct StandardMaterialUniform {
@@ -380,6 +389,9 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
380389
flags |= StandardMaterialFlags::ALPHA_MODE_MASK;
381390
}
382391
AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND,
392+
AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED,
393+
AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD,
394+
AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY,
383395
};
384396

385397
StandardMaterialUniform {

crates/bevy_pbr/src/prepass/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,10 @@ pub fn queue_prepass_material_meshes<M: Material>(
527527
match alpha_mode {
528528
AlphaMode::Opaque => {}
529529
AlphaMode::Mask(_) => mesh_key |= MeshPipelineKey::ALPHA_MASK,
530-
AlphaMode::Blend => continue,
530+
AlphaMode::Blend
531+
| AlphaMode::Premultiplied
532+
| AlphaMode::Add
533+
| AlphaMode::Multiply => continue,
531534
}
532535

533536
let pipeline_id = pipelines.specialize(
@@ -566,7 +569,10 @@ pub fn queue_prepass_material_meshes<M: Material>(
566569
distance,
567570
});
568571
}
569-
AlphaMode::Blend => {}
572+
AlphaMode::Blend
573+
| AlphaMode::Premultiplied
574+
| AlphaMode::Add
575+
| AlphaMode::Multiply => {}
570576
}
571577
}
572578
}

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,16 @@ bitflags::bitflags! {
557557
/// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA.
558558
pub struct MeshPipelineKey: u32 {
559559
const NONE = 0;
560-
const TRANSPARENT_MAIN_PASS = (1 << 0);
561-
const HDR = (1 << 1);
562-
const TONEMAP_IN_SHADER = (1 << 2);
563-
const DEBAND_DITHER = (1 << 3);
564-
const DEPTH_PREPASS = (1 << 4);
565-
const NORMAL_PREPASS = (1 << 5);
566-
const ALPHA_MASK = (1 << 6);
560+
const HDR = (1 << 0);
561+
const TONEMAP_IN_SHADER = (1 << 1);
562+
const DEBAND_DITHER = (1 << 2);
563+
const DEPTH_PREPASS = (1 << 3);
564+
const NORMAL_PREPASS = (1 << 4);
565+
const ALPHA_MASK = (1 << 5);
566+
const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state
567+
const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3
568+
const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); //
569+
const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more bits
567570
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
568571
const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS;
569572
}
@@ -573,7 +576,11 @@ impl MeshPipelineKey {
573576
const MSAA_MASK_BITS: u32 = 0b111;
574577
const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones();
575578
const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111;
576-
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3;
579+
const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 =
580+
Self::MSAA_SHIFT_BITS - Self::PRIMITIVE_TOPOLOGY_MASK_BITS.count_ones();
581+
const BLEND_MASK_BITS: u32 = 0b11;
582+
const BLEND_SHIFT_BITS: u32 =
583+
Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones();
577584

578585
pub fn from_msaa_samples(msaa_samples: u32) -> Self {
579586
let msaa_bits =
@@ -677,12 +684,30 @@ impl SpecializedMeshPipeline for MeshPipeline {
677684
let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?;
678685

679686
let (label, blend, depth_write_enabled);
680-
if key.contains(MeshPipelineKey::TRANSPARENT_MAIN_PASS) {
681-
label = "transparent_mesh_pipeline".into();
682-
blend = Some(BlendState::ALPHA_BLENDING);
687+
let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS);
688+
if pass == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA {
689+
label = "premultiplied_alpha_mesh_pipeline".into();
690+
blend = Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING);
691+
shader_defs.push("PREMULTIPLY_ALPHA".into());
692+
shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into());
683693
// For the transparent pass, fragments that are closer will be alpha blended
684694
// but their depth is not written to the depth buffer
685695
depth_write_enabled = false;
696+
} else if pass == MeshPipelineKey::BLEND_MULTIPLY {
697+
label = "multiply_mesh_pipeline".into();
698+
blend = Some(BlendState {
699+
color: BlendComponent {
700+
src_factor: BlendFactor::Dst,
701+
dst_factor: BlendFactor::OneMinusSrcAlpha,
702+
operation: BlendOperation::Add,
703+
},
704+
alpha: BlendComponent::OVER,
705+
});
706+
shader_defs.push("PREMULTIPLY_ALPHA".into());
707+
shader_defs.push("BLEND_MULTIPLY".into());
708+
// For the multiply pass, fragments that are closer will be alpha blended
709+
// but their depth is not written to the depth buffer
710+
depth_write_enabled = false;
686711
} else {
687712
label = "opaque_mesh_pipeline".into();
688713
blend = Some(BlendState::REPLACE);

crates/bevy_pbr/src/render/pbr.wgsl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
106106
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
107107
output_rgb = pow(output_rgb, vec3<f32>(2.2));
108108
output_color = vec4(output_rgb, output_color.a);
109+
#endif
110+
#ifdef PREMULTIPLY_ALPHA
111+
output_color = premultiply_alpha(material.flags, output_color);
109112
#endif
110113
return output_color;
111114
}

crates/bevy_pbr/src/render/pbr_functions.wgsl

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
fn alpha_discard(material: StandardMaterial, output_color: vec4<f32>) -> vec4<f32> {
99
var color = output_color;
10-
if (material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u {
10+
let alpha_mode = material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
11+
if alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
1112
// NOTE: If rendering as opaque, alpha should be ignored so set to 1.0
1213
color.a = 1.0;
13-
} else if (material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u {
14+
} else if alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK {
1415
if color.a >= material.alpha_cutoff {
1516
// NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque
1617
color.a = 1.0;
@@ -268,3 +269,66 @@ fn dither(color: vec4<f32>, pos: vec2<f32>) -> vec4<f32> {
268269
}
269270
#endif // DEBAND_DITHER
270271

272+
#ifdef PREMULTIPLY_ALPHA
273+
fn premultiply_alpha(standard_material_flags: u32, color: vec4<f32>) -> vec4<f32> {
274+
// `Blend`, `Premultiplied` and `Alpha` all share the same `BlendState`. Depending
275+
// on the alpha mode, we premultiply the color channels by the alpha channel value,
276+
// (and also optionally replace the alpha value with 0.0) so that the result produces
277+
// the desired blend mode when sent to the blending operation.
278+
#ifdef BLEND_PREMULTIPLIED_ALPHA
279+
// For `BlendState::PREMULTIPLIED_ALPHA_BLENDING` the blend function is:
280+
//
281+
// result = 1 * src_color + (1 - src_alpha) * dst_color
282+
let alpha_mode = standard_material_flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
283+
if (alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND) {
284+
// Here, we premultiply `src_color` by `src_alpha` (ahead of time, here in the shader)
285+
//
286+
// src_color *= src_alpha
287+
//
288+
// We end up with:
289+
//
290+
// result = 1 * (src_alpha * src_color) + (1 - src_alpha) * dst_color
291+
// result = src_alpha * src_color + (1 - src_alpha) * dst_color
292+
//
293+
// Which is the blend operation for regular alpha blending `BlendState::ALPHA_BLENDING`
294+
return vec4<f32>(color.rgb * color.a, color.a);
295+
} else if (alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD) {
296+
// Here, we premultiply `src_color` by `src_alpha`, and replace `src_alpha` with 0.0:
297+
//
298+
// src_color *= src_alpha
299+
// src_alpha = 0.0
300+
//
301+
// We end up with:
302+
//
303+
// result = 1 * (src_alpha * src_color) + (1 - 0) * dst_color
304+
// result = src_alpha * src_color + 1 * dst_color
305+
//
306+
// Which is the blend operation for additive blending
307+
return vec4<f32>(color.rgb * color.a, 0.0);
308+
} else {
309+
// Here, we don't do anything, so that we get premultiplied alpha blending. (As expected)
310+
return color.rgba;
311+
}
312+
#endif
313+
// `Multiply` uses its own `BlendState`, but we still need to premultiply here in the
314+
// shader so that we get correct results as we tweak the alpha channel
315+
#ifdef BLEND_MULTIPLY
316+
// The blend function is:
317+
//
318+
// result = dst_color * src_color + (1 - src_alpha) * dst_color
319+
//
320+
// We premultiply `src_color` by `src_alpha`:
321+
//
322+
// src_color *= src_alpha
323+
//
324+
// We end up with:
325+
//
326+
// result = dst_color * (src_color * src_alpha) + (1 - src_alpha) * dst_color
327+
// result = src_alpha * (src_color * dst_color) + (1 - src_alpha) * dst_color
328+
//
329+
// Which is the blend operation for multiplicative blending with arbitrary mixing
330+
// controlled by the source alpha channel
331+
return vec4<f32>(color.rgb * color.a, color.a);
332+
#endif
333+
}
334+
#endif

crates/bevy_pbr/src/render/pbr_types.wgsl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ let STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u;
1717
let STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u;
1818
let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
1919
let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u;
20-
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u;
21-
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u;
22-
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u;
23-
let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u;
24-
let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1024u;
20+
let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u;
21+
let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u;
22+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29)
23+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29)
24+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29)
25+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 1073741824u; // (2u32 << 29)
26+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 1610612736u; // (3u32 << 29)
27+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 2147483648u; // (4u32 << 29)
28+
let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 2684354560u; // (5u32 << 29)
29+
// ↑ To calculate/verify the values above, use the following playground:
30+
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7792f8dd6fc6a8d4d0b6b1776898a7f4
2531

2632
// Creates a StandardMaterial with default values
2733
fn standard_material_new() -> StandardMaterial {

0 commit comments

Comments
 (0)