Skip to content

Commit 8c8a2ba

Browse files
committed
Blend mode support.
1 parent 4b2f9c5 commit 8c8a2ba

File tree

13 files changed

+716
-59
lines changed

13 files changed

+716
-59
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ path = "examples/custom_material.rs"
128128
name = "input"
129129
path = "examples/input.rs"
130130

131+
[[example]]
132+
name = "blend_modes"
133+
path = "examples/blend_modes.rs"
134+
131135
[profile.wasm-release]
132136
inherits = "release"
133137
opt-level = "z"

crates/processing_ffi/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,44 @@ pub extern "C" fn processing_shear_y(graphics_id: u64, angle: f32) {
466466
error::check(|| graphics_record_command(graphics_entity, DrawCommand::ShearY { angle }));
467467
}
468468

469+
/// Set the blend mode.
470+
///
471+
/// Mode values: 0=BLEND, 1=ADD, 2=SUBTRACT, 3=DARKEST, 4=LIGHTEST,
472+
/// 5=DIFFERENCE, 6=EXCLUSION, 7=MULTIPLY, 8=SCREEN, 9=REPLACE
473+
#[unsafe(no_mangle)]
474+
pub extern "C" fn processing_set_blend_mode(graphics_id: u64, mode: u8) {
475+
error::clear_error();
476+
let graphics_entity = Entity::from_bits(graphics_id);
477+
let blend_state = processing::prelude::BlendMode::from(mode).to_blend_state();
478+
error::check(|| graphics_record_command(graphics_entity, DrawCommand::BlendMode(blend_state)));
479+
}
480+
481+
/// Set a custom blend mode by specifying individual blend components.
482+
///
483+
/// Each factor/operation is a u8 mapping to the WebGPU BlendFactor/BlendOperation enums.
484+
/// BlendFactor: 0=Zero, 1=One, 2=Src, 3=OneMinusSrc, 4=SrcAlpha, 5=OneMinusSrcAlpha,
485+
/// 6=Dst, 7=OneMinusDst, 8=DstAlpha, 9=OneMinusDstAlpha, 10=SrcAlphaSaturated
486+
/// BlendOperation: 0=Add, 1=Subtract, 2=ReverseSubtract, 3=Min, 4=Max
487+
#[unsafe(no_mangle)]
488+
pub extern "C" fn processing_set_custom_blend_mode(
489+
graphics_id: u64,
490+
color_src: u8,
491+
color_dst: u8,
492+
color_op: u8,
493+
alpha_src: u8,
494+
alpha_dst: u8,
495+
alpha_op: u8,
496+
) {
497+
error::clear_error();
498+
let graphics_entity = Entity::from_bits(graphics_id);
499+
let blend_state = custom_blend_state(
500+
color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op,
501+
);
502+
error::check(|| {
503+
graphics_record_command(graphics_entity, DrawCommand::BlendMode(Some(blend_state)))
504+
});
505+
}
506+
469507
/// Draw a rectangle.
470508
///
471509
/// SAFETY:

crates/processing_pyo3/src/graphics.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,63 @@ use pyo3::{
1818
types::{PyDict, PyTuple},
1919
};
2020

21+
use crate::glfw::GlfwContext;
22+
use crate::math::{extract_vec2, extract_vec3, extract_vec4};
23+
24+
// ---------------------------------------------------------------------------
25+
// BlendMode
26+
// ---------------------------------------------------------------------------
27+
28+
#[pyclass(name = "BlendMode")]
29+
#[derive(Clone)]
30+
pub struct PyBlendMode {
31+
pub(crate) blend_state: Option<bevy::render::render_resource::BlendState>,
32+
name: Option<&'static str>,
33+
}
34+
35+
impl PyBlendMode {
36+
pub(crate) fn from_preset(mode: BlendMode) -> Self {
37+
Self {
38+
blend_state: mode.to_blend_state(),
39+
name: Some(mode.name()),
40+
}
41+
}
42+
}
43+
44+
#[pymethods]
45+
impl PyBlendMode {
46+
/// Create a custom blend mode by specifying individual blend components.
47+
///
48+
/// All arguments are keyword-only. Use the blend factor constants (ZERO, ONE,
49+
/// SRC_COLOR, SRC_ALPHA, DST_COLOR, etc.) and blend operation constants
50+
/// (OP_ADD, OP_SUBTRACT, OP_REVERSE_SUBTRACT, OP_MIN, OP_MAX).
51+
#[new]
52+
#[pyo3(signature = (*, color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op))]
53+
fn new(
54+
color_src: u8,
55+
color_dst: u8,
56+
color_op: u8,
57+
alpha_src: u8,
58+
alpha_dst: u8,
59+
alpha_op: u8,
60+
) -> Self {
61+
Self {
62+
blend_state: Some(custom_blend_state(
63+
color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op,
64+
)),
65+
name: None,
66+
}
67+
}
68+
69+
fn __repr__(&self) -> String {
70+
match self.name {
71+
Some(name) => format!("BlendMode.{name}"),
72+
None => "BlendMode(custom)".to_string(),
73+
}
74+
}
75+
}
76+
77+
2178
#[pyclass(unsendable)]
2279
pub struct Surface {
2380
pub(crate) entity: Entity,
@@ -517,6 +574,11 @@ impl Graphics {
517574
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
518575
}
519576

577+
pub fn blend_mode(&self, mode: &PyBlendMode) -> PyResult<()> {
578+
graphics_record_command(self.entity, DrawCommand::BlendMode(mode.blend_state))
579+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
580+
}
581+
520582
pub fn set_material(&self, material: &crate::material::Material) -> PyResult<()> {
521583
graphics_record_command(self.entity, DrawCommand::Material(material.entity))
522584
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))

crates/processing_pyo3/src/lib.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ pub(crate) mod shader;
2020
#[cfg(feature = "webcam")]
2121
mod webcam;
2222

23-
use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut};
23+
use graphics::{
24+
Geometry, Graphics, Image, Light, PyBlendMode, Topology, get_graphics, get_graphics_mut,
25+
};
2426
use material::Material;
2527

2628
use pyo3::{
@@ -127,6 +129,8 @@ mod mewnala {
127129
#[pymodule_export]
128130
use super::Material;
129131
#[pymodule_export]
132+
use super::PyBlendMode;
133+
#[pymodule_export]
130134
use super::Shader;
131135
#[pymodule_export]
132136
use super::Topology;
@@ -341,6 +345,62 @@ mod mewnala {
341345
#[pymodule_export]
342346
const XYZ: u8 = 9;
343347

348+
// Blend factor constants (for BlendMode custom constructor)
349+
#[pymodule_export]
350+
const ZERO: u8 = 0;
351+
#[pymodule_export]
352+
const ONE: u8 = 1;
353+
#[pymodule_export]
354+
const SRC_COLOR: u8 = 2;
355+
#[pymodule_export]
356+
const ONE_MINUS_SRC_COLOR: u8 = 3;
357+
#[pymodule_export]
358+
const SRC_ALPHA: u8 = 4;
359+
#[pymodule_export]
360+
const ONE_MINUS_SRC_ALPHA: u8 = 5;
361+
#[pymodule_export]
362+
const DST_COLOR: u8 = 6;
363+
#[pymodule_export]
364+
const ONE_MINUS_DST_COLOR: u8 = 7;
365+
#[pymodule_export]
366+
const DST_ALPHA: u8 = 8;
367+
#[pymodule_export]
368+
const ONE_MINUS_DST_ALPHA: u8 = 9;
369+
#[pymodule_export]
370+
const SRC_ALPHA_SATURATED: u8 = 10;
371+
372+
// Blend operation constants (for BlendMode custom constructor)
373+
#[pymodule_export]
374+
const OP_ADD: u8 = 0;
375+
#[pymodule_export]
376+
const OP_SUBTRACT: u8 = 1;
377+
#[pymodule_export]
378+
const OP_REVERSE_SUBTRACT: u8 = 2;
379+
#[pymodule_export]
380+
const OP_MIN: u8 = 3;
381+
#[pymodule_export]
382+
const OP_MAX: u8 = 4;
383+
384+
// Blend mode preset constants (added in pymodule_init)
385+
#[pymodule_init]
386+
fn init(module: &Bound<'_, PyModule>) -> PyResult<()> {
387+
use processing::prelude::BlendMode;
388+
module.add("BLEND", PyBlendMode::from_preset(BlendMode::Blend))?;
389+
module.add("ADD", PyBlendMode::from_preset(BlendMode::Add))?;
390+
module.add("SUBTRACT", PyBlendMode::from_preset(BlendMode::Subtract))?;
391+
module.add("DARKEST", PyBlendMode::from_preset(BlendMode::Darkest))?;
392+
module.add("LIGHTEST", PyBlendMode::from_preset(BlendMode::Lightest))?;
393+
module.add(
394+
"DIFFERENCE",
395+
PyBlendMode::from_preset(BlendMode::Difference),
396+
)?;
397+
module.add("EXCLUSION", PyBlendMode::from_preset(BlendMode::Exclusion))?;
398+
module.add("MULTIPLY", PyBlendMode::from_preset(BlendMode::Multiply))?;
399+
module.add("SCREEN", PyBlendMode::from_preset(BlendMode::Screen))?;
400+
module.add("REPLACE", PyBlendMode::from_preset(BlendMode::Replace))?;
401+
Ok(())
402+
}
403+
344404
#[pymodule]
345405
mod math {
346406
use super::*;
@@ -804,6 +864,12 @@ mod mewnala {
804864
graphics!(module).stroke_join(join)
805865
}
806866

867+
#[pyfunction]
868+
#[pyo3(pass_module, signature = (mode))]
869+
fn blend_mode(module: &Bound<'_, PyModule>, mode: &Bound<'_, PyBlendMode>) -> PyResult<()> {
870+
graphics!(module).blend_mode(&*mode.extract::<PyRef<PyBlendMode>>()?)
871+
}
872+
807873
#[pyfunction]
808874
#[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
809875
fn rect(

crates/processing_render/src/graphics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ impl ProcessingProjection {
132132

133133
impl CameraProjection for ProcessingProjection {
134134
fn get_clip_from_view(&self) -> Mat4 {
135+
// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
136+
// This is for interoperability with Bevy's reverse-Z depth pipeline.
135137
Mat4::orthographic_rh(
136138
0.0,
137139
self.width,

crates/processing_render/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub struct ProcessingRenderPlugin;
3434

3535
impl Plugin for ProcessingRenderPlugin {
3636
fn build(&self, app: &mut App) {
37-
use render::material::{add_custom_materials, add_standard_materials};
37+
use render::material::{add_custom_materials, add_processing_materials};
3838
use render::{activate_cameras, clear_transient_meshes, flush_draw_commands};
3939

4040
let config = app.world().resource::<Config>().clone();
@@ -66,7 +66,7 @@ impl Plugin for ProcessingRenderPlugin {
6666
surface::SurfacePlugin,
6767
geometry::GeometryPlugin,
6868
light::LightPlugin,
69-
material::MaterialPlugin,
69+
material::ProcessingMaterialPlugin,
7070
bevy::pbr::wireframe::WireframePlugin::default(),
7171
material::custom::CustomMaterialPlugin,
7272
));
@@ -76,7 +76,7 @@ impl Plugin for ProcessingRenderPlugin {
7676
Update,
7777
(
7878
flush_draw_commands,
79-
add_standard_materials,
79+
add_processing_materials,
8080
add_custom_materials,
8181
)
8282
.chain()

crates/processing_render/src/material/custom.rs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@ wesl::wesl_pkg!(lygia);
1111

1212
use bevy::{
1313
asset::{AsAssetId, AssetEventSystems},
14-
core_pipeline::core_3d::Opaque3d,
14+
core_pipeline::core_3d::{Opaque3d, Transparent3d},
1515
ecs::system::{
1616
SystemParamItem,
1717
lifetimeless::{SRes, SResMut},
1818
},
19-
material::{MaterialProperties, key::ErasedMeshPipelineKey},
19+
material::{
20+
MaterialProperties,
21+
descriptor::RenderPipelineDescriptor,
22+
key::{ErasedMaterialKey, ErasedMaterialPipelineKey, ErasedMeshPipelineKey},
23+
specialize::SpecializedMeshPipelineError,
24+
},
25+
mesh::MeshVertexBufferLayoutRef,
2026
pbr::{
2127
DrawMaterial, EntitiesNeedingSpecialization, MainPassOpaqueDrawFunction,
22-
MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialFragmentShader,
23-
MaterialVertexShader, MeshPipelineKey, PreparedMaterial, RenderMaterialBindings,
24-
RenderMaterialInstance, RenderMaterialInstances, base_specialize,
28+
MainPassTransparentDrawFunction, MaterialBindGroupAllocator, MaterialBindGroupAllocators,
29+
MaterialFragmentShader, MaterialVertexShader, MeshPipelineKey, PreparedMaterial,
30+
RenderMaterialBindings, RenderMaterialInstance, RenderMaterialInstances, base_specialize,
2531
},
2632
prelude::*,
2733
reflect::{PartialReflect, ReflectMut, ReflectRef, structs::Struct},
@@ -31,7 +37,9 @@ use bevy::{
3137
erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError},
3238
render_asset::RenderAssets,
3339
render_phase::DrawFunctions,
34-
render_resource::{BindGroupLayoutDescriptor, BindingResources, UnpreparedBindGroup},
40+
render_resource::{
41+
BindGroupLayoutDescriptor, BindingResources, BlendState, UnpreparedBindGroup,
42+
},
3543
renderer::RenderDevice,
3644
sync_world::MainEntity,
3745
texture::GpuImage,
@@ -42,17 +50,43 @@ use bevy_naga_reflect::dynamic_shader::DynamicShader;
4250

4351
use bevy::shader::Shader as ShaderAsset;
4452

53+
use std::any::Any;
54+
4555
use crate::material::MaterialValue;
4656
use crate::render::material::UntypedMaterial;
4757
use processing_core::config::{Config, ConfigKey};
4858
use processing_core::error::{ProcessingError, Result};
4959

60+
#[derive(Clone, Hash, PartialEq)]
61+
struct CustomMaterialKey {
62+
blend_state: Option<BlendState>,
63+
}
64+
65+
fn custom_blend_specialize(
66+
key: &dyn Any,
67+
descriptor: &mut RenderPipelineDescriptor,
68+
_layout: &MeshVertexBufferLayoutRef,
69+
_pipeline_key: ErasedMaterialPipelineKey,
70+
) -> std::result::Result<(), SpecializedMeshPipelineError> {
71+
if let Some(key) = key.downcast_ref::<CustomMaterialKey>() {
72+
if let Some(blend_state) = key.blend_state {
73+
if let Some(fragment_state) = &mut descriptor.fragment {
74+
for target in fragment_state.targets.iter_mut().flatten() {
75+
target.blend = Some(blend_state);
76+
}
77+
}
78+
}
79+
}
80+
Ok(())
81+
}
82+
5083
#[derive(Asset, TypePath, Clone)]
5184
pub struct CustomMaterial {
5285
pub shader: DynamicShader,
5386
pub shader_handle: Handle<ShaderAsset>,
5487
pub has_vertex: bool,
5588
pub has_fragment: bool,
89+
pub blend_state: Option<bevy::render::render_resource::BlendState>,
5690
}
5791

5892
#[derive(Component)]
@@ -227,6 +261,7 @@ pub fn create_custom(
227261
shader_handle: program.shader_handle.clone(),
228262
has_vertex,
229263
has_fragment,
264+
blend_state: None,
230265
};
231266
let handle = custom_materials.add(material);
232267
Ok(commands.spawn(UntypedMaterial(handle.untyped())).id())
@@ -393,13 +428,22 @@ impl ErasedRenderAsset for CustomMaterial {
393428

394429
let draw_function = opaque_draw_functions.read().id::<DrawMaterial>();
395430

431+
let blend_state = source_asset.blend_state;
396432
let mut properties = MaterialProperties {
397433
mesh_pipeline_key_bits: ErasedMeshPipelineKey::new(MeshPipelineKey::empty()),
398434
base_specialize: Some(base_specialize),
399435
material_layout: Some(bind_group_layout),
436+
material_key: ErasedMaterialKey::new(CustomMaterialKey { blend_state }),
437+
user_specialize: Some(custom_blend_specialize),
438+
alpha_mode: if blend_state.is_some() {
439+
AlphaMode::Blend
440+
} else {
441+
AlphaMode::Opaque
442+
},
400443
..Default::default()
401444
};
402445
properties.add_draw_function(MainPassOpaqueDrawFunction, draw_function);
446+
properties.add_draw_function(MainPassTransparentDrawFunction, draw_function);
403447
if source_asset.has_vertex {
404448
properties.add_shader(MaterialVertexShader, source_asset.shader_handle.clone());
405449
}

0 commit comments

Comments
 (0)