From 8c8a2ba8e11c84a9d1b05ec8ed1532139cbbfe69 Mon Sep 17 00:00:00 2001 From: charlotte Date: Fri, 27 Mar 2026 16:50:18 -0700 Subject: [PATCH 1/4] Blend mode support. --- Cargo.toml | 4 + crates/processing_ffi/src/lib.rs | 38 ++++ crates/processing_pyo3/src/graphics.rs | 62 ++++++ crates/processing_pyo3/src/lib.rs | 68 ++++++- crates/processing_render/src/graphics.rs | 2 + crates/processing_render/src/lib.rs | 6 +- .../processing_render/src/material/custom.rs | 56 +++++- crates/processing_render/src/material/mod.rs | 74 +++++++- .../processing_render/src/render/command.rs | 176 ++++++++++++++++++ .../processing_render/src/render/material.rs | 75 +++++--- crates/processing_render/src/render/mod.rs | 95 ++++++++-- examples/blend_modes.rs | 117 ++++++++++++ src/prelude.rs | 2 +- 13 files changed, 716 insertions(+), 59 deletions(-) create mode 100644 examples/blend_modes.rs diff --git a/Cargo.toml b/Cargo.toml index 969a15d..35b4b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -128,6 +128,10 @@ path = "examples/custom_material.rs" name = "input" path = "examples/input.rs" +[[example]] +name = "blend_modes" +path = "examples/blend_modes.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 8bc9efe..bd45c7b 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -466,6 +466,44 @@ pub extern "C" fn processing_shear_y(graphics_id: u64, angle: f32) { error::check(|| graphics_record_command(graphics_entity, DrawCommand::ShearY { angle })); } +/// Set the blend mode. +/// +/// Mode values: 0=BLEND, 1=ADD, 2=SUBTRACT, 3=DARKEST, 4=LIGHTEST, +/// 5=DIFFERENCE, 6=EXCLUSION, 7=MULTIPLY, 8=SCREEN, 9=REPLACE +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_blend_mode(graphics_id: u64, mode: u8) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + let blend_state = processing::prelude::BlendMode::from(mode).to_blend_state(); + error::check(|| graphics_record_command(graphics_entity, DrawCommand::BlendMode(blend_state))); +} + +/// Set a custom blend mode by specifying individual blend components. +/// +/// Each factor/operation is a u8 mapping to the WebGPU BlendFactor/BlendOperation enums. +/// BlendFactor: 0=Zero, 1=One, 2=Src, 3=OneMinusSrc, 4=SrcAlpha, 5=OneMinusSrcAlpha, +/// 6=Dst, 7=OneMinusDst, 8=DstAlpha, 9=OneMinusDstAlpha, 10=SrcAlphaSaturated +/// BlendOperation: 0=Add, 1=Subtract, 2=ReverseSubtract, 3=Min, 4=Max +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_custom_blend_mode( + graphics_id: u64, + color_src: u8, + color_dst: u8, + color_op: u8, + alpha_src: u8, + alpha_dst: u8, + alpha_op: u8, +) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + let blend_state = custom_blend_state( + color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, + ); + error::check(|| { + graphics_record_command(graphics_entity, DrawCommand::BlendMode(Some(blend_state))) + }); +} + /// Draw a rectangle. /// /// SAFETY: diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 92a2144..44e1cd2 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -18,6 +18,63 @@ use pyo3::{ types::{PyDict, PyTuple}, }; +use crate::glfw::GlfwContext; +use crate::math::{extract_vec2, extract_vec3, extract_vec4}; + +// --------------------------------------------------------------------------- +// BlendMode +// --------------------------------------------------------------------------- + +#[pyclass(name = "BlendMode")] +#[derive(Clone)] +pub struct PyBlendMode { + pub(crate) blend_state: Option, + name: Option<&'static str>, +} + +impl PyBlendMode { + pub(crate) fn from_preset(mode: BlendMode) -> Self { + Self { + blend_state: mode.to_blend_state(), + name: Some(mode.name()), + } + } +} + +#[pymethods] +impl PyBlendMode { + /// Create a custom blend mode by specifying individual blend components. + /// + /// All arguments are keyword-only. Use the blend factor constants (ZERO, ONE, + /// SRC_COLOR, SRC_ALPHA, DST_COLOR, etc.) and blend operation constants + /// (OP_ADD, OP_SUBTRACT, OP_REVERSE_SUBTRACT, OP_MIN, OP_MAX). + #[new] + #[pyo3(signature = (*, color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op))] + fn new( + color_src: u8, + color_dst: u8, + color_op: u8, + alpha_src: u8, + alpha_dst: u8, + alpha_op: u8, + ) -> Self { + Self { + blend_state: Some(custom_blend_state( + color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, + )), + name: None, + } + } + + fn __repr__(&self) -> String { + match self.name { + Some(name) => format!("BlendMode.{name}"), + None => "BlendMode(custom)".to_string(), + } + } +} + + #[pyclass(unsendable)] pub struct Surface { pub(crate) entity: Entity, @@ -517,6 +574,11 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + pub fn blend_mode(&self, mode: &PyBlendMode) -> PyResult<()> { + graphics_record_command(self.entity, DrawCommand::BlendMode(mode.blend_state)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn set_material(&self, material: &crate::material::Material) -> PyResult<()> { graphics_record_command(self.entity, DrawCommand::Material(material.entity)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index b3227c8..ee0af25 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -20,7 +20,9 @@ pub(crate) mod shader; #[cfg(feature = "webcam")] mod webcam; -use graphics::{Geometry, Graphics, Image, Light, Topology, get_graphics, get_graphics_mut}; +use graphics::{ + Geometry, Graphics, Image, Light, PyBlendMode, Topology, get_graphics, get_graphics_mut, +}; use material::Material; use pyo3::{ @@ -127,6 +129,8 @@ mod mewnala { #[pymodule_export] use super::Material; #[pymodule_export] + use super::PyBlendMode; + #[pymodule_export] use super::Shader; #[pymodule_export] use super::Topology; @@ -341,6 +345,62 @@ mod mewnala { #[pymodule_export] const XYZ: u8 = 9; + // Blend factor constants (for BlendMode custom constructor) + #[pymodule_export] + const ZERO: u8 = 0; + #[pymodule_export] + const ONE: u8 = 1; + #[pymodule_export] + const SRC_COLOR: u8 = 2; + #[pymodule_export] + const ONE_MINUS_SRC_COLOR: u8 = 3; + #[pymodule_export] + const SRC_ALPHA: u8 = 4; + #[pymodule_export] + const ONE_MINUS_SRC_ALPHA: u8 = 5; + #[pymodule_export] + const DST_COLOR: u8 = 6; + #[pymodule_export] + const ONE_MINUS_DST_COLOR: u8 = 7; + #[pymodule_export] + const DST_ALPHA: u8 = 8; + #[pymodule_export] + const ONE_MINUS_DST_ALPHA: u8 = 9; + #[pymodule_export] + const SRC_ALPHA_SATURATED: u8 = 10; + + // Blend operation constants (for BlendMode custom constructor) + #[pymodule_export] + const OP_ADD: u8 = 0; + #[pymodule_export] + const OP_SUBTRACT: u8 = 1; + #[pymodule_export] + const OP_REVERSE_SUBTRACT: u8 = 2; + #[pymodule_export] + const OP_MIN: u8 = 3; + #[pymodule_export] + const OP_MAX: u8 = 4; + + // Blend mode preset constants (added in pymodule_init) + #[pymodule_init] + fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { + use processing::prelude::BlendMode; + module.add("BLEND", PyBlendMode::from_preset(BlendMode::Blend))?; + module.add("ADD", PyBlendMode::from_preset(BlendMode::Add))?; + module.add("SUBTRACT", PyBlendMode::from_preset(BlendMode::Subtract))?; + module.add("DARKEST", PyBlendMode::from_preset(BlendMode::Darkest))?; + module.add("LIGHTEST", PyBlendMode::from_preset(BlendMode::Lightest))?; + module.add( + "DIFFERENCE", + PyBlendMode::from_preset(BlendMode::Difference), + )?; + module.add("EXCLUSION", PyBlendMode::from_preset(BlendMode::Exclusion))?; + module.add("MULTIPLY", PyBlendMode::from_preset(BlendMode::Multiply))?; + module.add("SCREEN", PyBlendMode::from_preset(BlendMode::Screen))?; + module.add("REPLACE", PyBlendMode::from_preset(BlendMode::Replace))?; + Ok(()) + } + #[pymodule] mod math { use super::*; @@ -804,6 +864,12 @@ mod mewnala { graphics!(module).stroke_join(join) } + #[pyfunction] + #[pyo3(pass_module, signature = (mode))] + fn blend_mode(module: &Bound<'_, PyModule>, mode: &Bound<'_, PyBlendMode>) -> PyResult<()> { + graphics!(module).blend_mode(&*mode.extract::>()?) + } + #[pyfunction] #[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))] fn rect( diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 7877d16..0497424 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -132,6 +132,8 @@ impl ProcessingProjection { impl CameraProjection for ProcessingProjection { fn get_clip_from_view(&self) -> Mat4 { + // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] + // This is for interoperability with Bevy's reverse-Z depth pipeline. Mat4::orthographic_rh( 0.0, self.width, diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 2eb6954..1908bcd 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -34,7 +34,7 @@ pub struct ProcessingRenderPlugin; impl Plugin for ProcessingRenderPlugin { fn build(&self, app: &mut App) { - use render::material::{add_custom_materials, add_standard_materials}; + use render::material::{add_custom_materials, add_processing_materials}; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; let config = app.world().resource::().clone(); @@ -66,7 +66,7 @@ impl Plugin for ProcessingRenderPlugin { surface::SurfacePlugin, geometry::GeometryPlugin, light::LightPlugin, - material::MaterialPlugin, + material::ProcessingMaterialPlugin, bevy::pbr::wireframe::WireframePlugin::default(), material::custom::CustomMaterialPlugin, )); @@ -76,7 +76,7 @@ impl Plugin for ProcessingRenderPlugin { Update, ( flush_draw_commands, - add_standard_materials, + add_processing_materials, add_custom_materials, ) .chain() diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index 56617b1..7d07bbf 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -11,17 +11,23 @@ wesl::wesl_pkg!(lygia); use bevy::{ asset::{AsAssetId, AssetEventSystems}, - core_pipeline::core_3d::Opaque3d, + core_pipeline::core_3d::{Opaque3d, Transparent3d}, ecs::system::{ SystemParamItem, lifetimeless::{SRes, SResMut}, }, - material::{MaterialProperties, key::ErasedMeshPipelineKey}, + material::{ + MaterialProperties, + descriptor::RenderPipelineDescriptor, + key::{ErasedMaterialKey, ErasedMaterialPipelineKey, ErasedMeshPipelineKey}, + specialize::SpecializedMeshPipelineError, + }, + mesh::MeshVertexBufferLayoutRef, pbr::{ DrawMaterial, EntitiesNeedingSpecialization, MainPassOpaqueDrawFunction, - MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialFragmentShader, - MaterialVertexShader, MeshPipelineKey, PreparedMaterial, RenderMaterialBindings, - RenderMaterialInstance, RenderMaterialInstances, base_specialize, + MainPassTransparentDrawFunction, MaterialBindGroupAllocator, MaterialBindGroupAllocators, + MaterialFragmentShader, MaterialVertexShader, MeshPipelineKey, PreparedMaterial, + RenderMaterialBindings, RenderMaterialInstance, RenderMaterialInstances, base_specialize, }, prelude::*, reflect::{PartialReflect, ReflectMut, ReflectRef, structs::Struct}, @@ -31,7 +37,9 @@ use bevy::{ erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError}, render_asset::RenderAssets, render_phase::DrawFunctions, - render_resource::{BindGroupLayoutDescriptor, BindingResources, UnpreparedBindGroup}, + render_resource::{ + BindGroupLayoutDescriptor, BindingResources, BlendState, UnpreparedBindGroup, + }, renderer::RenderDevice, sync_world::MainEntity, texture::GpuImage, @@ -42,17 +50,43 @@ use bevy_naga_reflect::dynamic_shader::DynamicShader; use bevy::shader::Shader as ShaderAsset; +use std::any::Any; + use crate::material::MaterialValue; use crate::render::material::UntypedMaterial; use processing_core::config::{Config, ConfigKey}; use processing_core::error::{ProcessingError, Result}; +#[derive(Clone, Hash, PartialEq)] +struct CustomMaterialKey { + blend_state: Option, +} + +fn custom_blend_specialize( + key: &dyn Any, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _pipeline_key: ErasedMaterialPipelineKey, +) -> std::result::Result<(), SpecializedMeshPipelineError> { + if let Some(key) = key.downcast_ref::() { + if let Some(blend_state) = key.blend_state { + if let Some(fragment_state) = &mut descriptor.fragment { + for target in fragment_state.targets.iter_mut().flatten() { + target.blend = Some(blend_state); + } + } + } + } + Ok(()) +} + #[derive(Asset, TypePath, Clone)] pub struct CustomMaterial { pub shader: DynamicShader, pub shader_handle: Handle, pub has_vertex: bool, pub has_fragment: bool, + pub blend_state: Option, } #[derive(Component)] @@ -227,6 +261,7 @@ pub fn create_custom( shader_handle: program.shader_handle.clone(), has_vertex, has_fragment, + blend_state: None, }; let handle = custom_materials.add(material); Ok(commands.spawn(UntypedMaterial(handle.untyped())).id()) @@ -393,13 +428,22 @@ impl ErasedRenderAsset for CustomMaterial { let draw_function = opaque_draw_functions.read().id::(); + let blend_state = source_asset.blend_state; let mut properties = MaterialProperties { mesh_pipeline_key_bits: ErasedMeshPipelineKey::new(MeshPipelineKey::empty()), base_specialize: Some(base_specialize), material_layout: Some(bind_group_layout), + material_key: ErasedMaterialKey::new(CustomMaterialKey { blend_state }), + user_specialize: Some(custom_blend_specialize), + alpha_mode: if blend_state.is_some() { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, ..Default::default() }; properties.add_draw_function(MainPassOpaqueDrawFunction, draw_function); + properties.add_draw_function(MainPassTransparentDrawFunction, draw_function); if source_asset.has_vertex { properties.add_shader(MaterialVertexShader, source_asset.shader_handle.clone()); } diff --git a/crates/processing_render/src/material/mod.rs b/crates/processing_render/src/material/mod.rs index 288751a..07d737a 100644 --- a/crates/processing_render/src/material/mod.rs +++ b/crates/processing_render/src/material/mod.rs @@ -1,15 +1,26 @@ pub mod custom; pub mod pbr; -use bevy::prelude::*; - use crate::render::material::UntypedMaterial; -use processing_core::error::{ProcessingError, Result}; +use bevy::material::descriptor::RenderPipelineDescriptor; +use bevy::material::specialize::SpecializedMeshPipelineError; +use bevy::mesh::MeshVertexBufferLayoutRef; +use bevy::pbr::{ + ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline, +}; +use bevy::prelude::*; +use bevy::render::render_resource::{AsBindGroup, BlendState}; +use bevy::shader::ShaderRef; +use processing_core::error::ProcessingError; -pub struct MaterialPlugin; +pub struct ProcessingMaterialPlugin; -impl Plugin for MaterialPlugin { +impl Plugin for ProcessingMaterialPlugin { fn build(&self, app: &mut App) { + app.add_plugins(bevy::pbr::MaterialPlugin::< + ExtendedMaterial, + >::default()); + let world = app.world_mut(); let handle = world .resource_mut::>() @@ -59,7 +70,7 @@ pub fn set_property( material_handles: Query<&UntypedMaterial>, mut standard_materials: ResMut>, mut custom_materials: ResMut>, -) -> Result<()> { +) -> processing_core::error::Result<()> { let untyped = material_handles .get(entity) .map_err(|_| ProcessingError::MaterialNotFound)?; @@ -89,7 +100,7 @@ pub fn destroy( material_handles: Query<&UntypedMaterial>, mut standard_materials: ResMut>, mut custom_materials: ResMut>, -) -> Result<()> { +) -> processing_core::error::Result<()> { let untyped = material_handles .get(entity) .map_err(|_| ProcessingError::MaterialNotFound)?; @@ -102,3 +113,52 @@ pub fn destroy( commands.entity(entity).despawn(); Ok(()) } + +#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)] +#[bind_group_data(ProcessingMaterialKey)] +pub struct ProcessingMaterial { + pub blend_state: Option, +} + +#[repr(C)] +#[derive(Eq, PartialEq, Hash, Copy, Clone)] +pub struct ProcessingMaterialKey { + blend_state: Option, +} + +impl From<&ProcessingMaterial> for ProcessingMaterialKey { + fn from(mat: &ProcessingMaterial) -> Self { + ProcessingMaterialKey { + blend_state: mat.blend_state, + } + } +} + +impl MaterialExtension for ProcessingMaterial { + fn vertex_shader() -> ShaderRef { + ::vertex_shader() + } + + fn fragment_shader() -> ShaderRef { + ::fragment_shader() + } + + fn specialize( + _pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + key: MaterialExtensionKey, + ) -> std::result::Result<(), SpecializedMeshPipelineError> { + if let Some(blend_state) = key.bind_group_data.blend_state { + // this should never be null but we have to check it anyway + if let Some(fragment_state) = &mut descriptor.fragment { + fragment_state.targets.iter_mut().for_each(|target| { + if let Some(target) = target { + target.blend = Some(blend_state); + } + }); + } + } + Ok(()) + } +} diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 19016e0..92cd957 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use bevy::render::render_resource::{BlendComponent, BlendFactor, BlendOperation, BlendState}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] @@ -40,6 +41,180 @@ impl From for StrokeJoinMode { } } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum BlendMode { + #[default] + Blend = 0, + Add = 1, + Subtract = 2, + Darkest = 3, + Lightest = 4, + Difference = 5, + Exclusion = 6, + Multiply = 7, + Screen = 8, + Replace = 9, +} + +impl From for BlendMode { + fn from(v: u8) -> Self { + match v { + 0 => Self::Blend, + 1 => Self::Add, + 2 => Self::Subtract, + 3 => Self::Darkest, + 4 => Self::Lightest, + 5 => Self::Difference, + 6 => Self::Exclusion, + 7 => Self::Multiply, + 8 => Self::Screen, + 9 => Self::Replace, + _ => Self::default(), + } + } +} + +/// Convert a u8 to a WebGPU BlendFactor. +pub fn blend_factor_from_u8(v: u8) -> BlendFactor { + match v { + 0 => BlendFactor::Zero, + 1 => BlendFactor::One, + 2 => BlendFactor::Src, + 3 => BlendFactor::OneMinusSrc, + 4 => BlendFactor::SrcAlpha, + 5 => BlendFactor::OneMinusSrcAlpha, + 6 => BlendFactor::Dst, + 7 => BlendFactor::OneMinusDst, + 8 => BlendFactor::DstAlpha, + 9 => BlendFactor::OneMinusDstAlpha, + 10 => BlendFactor::SrcAlphaSaturated, + _ => BlendFactor::One, + } +} + +/// Convert a u8 to a WebGPU BlendOperation. +pub fn blend_op_from_u8(v: u8) -> BlendOperation { + match v { + 0 => BlendOperation::Add, + 1 => BlendOperation::Subtract, + 2 => BlendOperation::ReverseSubtract, + 3 => BlendOperation::Min, + 4 => BlendOperation::Max, + _ => BlendOperation::Add, + } +} + +/// Build a BlendState from individual component parameters. +pub fn custom_blend_state( + color_src: u8, + color_dst: u8, + color_op: u8, + alpha_src: u8, + alpha_dst: u8, + alpha_op: u8, +) -> BlendState { + BlendState { + color: BlendComponent { + src_factor: blend_factor_from_u8(color_src), + dst_factor: blend_factor_from_u8(color_dst), + operation: blend_op_from_u8(color_op), + }, + alpha: BlendComponent { + src_factor: blend_factor_from_u8(alpha_src), + dst_factor: blend_factor_from_u8(alpha_dst), + operation: blend_op_from_u8(alpha_op), + }, + } +} + +const ALPHA_ADDITIVE: BlendComponent = BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, +}; + +impl BlendMode { + pub fn name(self) -> &'static str { + match self { + Self::Blend => "BLEND", + Self::Add => "ADD", + Self::Subtract => "SUBTRACT", + Self::Darkest => "DARKEST", + Self::Lightest => "LIGHTEST", + Self::Difference => "DIFFERENCE", + Self::Exclusion => "EXCLUSION", + Self::Multiply => "MULTIPLY", + Self::Screen => "SCREEN", + Self::Replace => "REPLACE", + } + } + + /// Convert to a WebGPU BlendState, matching Processing's OpenGL blend configurations. + /// Returns None for the default Blend mode (standard alpha blending handled by AlphaMode). + pub fn to_blend_state(self) -> Option { + use BlendFactor::*; + use BlendOperation::*; + + let color = |src_factor, dst_factor, operation| BlendComponent { + src_factor, + dst_factor, + operation, + }; + + match self { + Self::Blend => None, + Self::Add => Some(BlendState { + color: color(SrcAlpha, One, Add), + alpha: ALPHA_ADDITIVE, + }), + Self::Subtract => Some(BlendState { + color: color(SrcAlpha, One, ReverseSubtract), + alpha: ALPHA_ADDITIVE, + }), + // Blend factors are ignored by Min/Max operations + Self::Darkest => Some(BlendState { + color: color(One, One, Min), + alpha: ALPHA_ADDITIVE, + }), + Self::Lightest => Some(BlendState { + color: color(One, One, Max), + alpha: ALPHA_ADDITIVE, + }), + // |src - dst| — not representable with fixed-function blending; + // reverse subtract is the same approximation Processing's OpenGL renderer uses. + Self::Difference => Some(BlendState { + color: color(One, One, ReverseSubtract), + alpha: BlendComponent { + src_factor: One, + dst_factor: One, + operation: Max, + }, + }), + // src + dst - 2*src*dst = (1-dst)*src + (1-src)*dst + Self::Exclusion => Some(BlendState { + color: color(OneMinusDst, OneMinusSrc, Add), + alpha: BlendComponent { + src_factor: One, + dst_factor: OneMinusSrcAlpha, + operation: Add, + }, + }), + // src * dst (alpha-aware: falls back to dst when src is transparent) + Self::Multiply => Some(BlendState { + color: color(Dst, OneMinusSrcAlpha, Add), + alpha: ALPHA_ADDITIVE, + }), + // src + dst - src*dst = (1-dst)*src + dst + Self::Screen => Some(BlendState { + color: color(OneMinusDst, One, Add), + alpha: ALPHA_ADDITIVE, + }), + Self::Replace => Some(BlendState::REPLACE), + } + } +} + #[derive(Debug, Clone)] pub enum DrawCommand { BackgroundColor(Color), @@ -77,6 +252,7 @@ pub enum DrawCommand { angle: f32, }, Geometry(Entity), + BlendMode(Option), Material(Entity), Box { width: f32, diff --git a/crates/processing_render/src/render/material.rs b/crates/processing_render/src/render/material.rs index bdbc67b..8744a8f 100644 --- a/crates/processing_render/src/render/material.rs +++ b/crates/processing_render/src/render/material.rs @@ -1,52 +1,69 @@ +use bevy::pbr::ExtendedMaterial; use bevy::prelude::*; +use bevy::render::render_resource::BlendState; use std::ops::Deref; +use crate::material::ProcessingMaterial; use crate::material::custom::{CustomMaterial, CustomMaterial3d}; #[derive(Component, Deref)] pub struct UntypedMaterial(pub UntypedHandle); +pub type ProcessingExtendedMaterial = ExtendedMaterial; + #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub enum MaterialKey { Color { transparent: bool, background_image: Option>, + blend_state: Option, }, Pbr { albedo: [u8; 4], roughness: u8, metallic: u8, emissive: [u8; 4], + blend_state: Option, + }, + Custom { + entity: Entity, + blend_state: Option, }, - Custom(Entity), } impl MaterialKey { - pub fn to_material(&self, materials: &mut ResMut>) -> UntypedHandle { + pub fn blend_state(&self) -> Option { + match self { + MaterialKey::Color { blend_state, .. } => *blend_state, + MaterialKey::Pbr { blend_state, .. } => *blend_state, + MaterialKey::Custom { blend_state, .. } => *blend_state, + } + } + + pub fn to_standard_material(&self) -> StandardMaterial { match self { MaterialKey::Color { transparent, background_image, - } => { - let mat = StandardMaterial { - base_color: Color::WHITE, - unlit: true, - cull_mode: None, - base_color_texture: background_image.clone(), - alpha_mode: if *transparent { - AlphaMode::Blend - } else { - AlphaMode::Opaque - }, - ..default() - }; - materials.add(mat).untyped() - } + blend_state, + } => StandardMaterial { + base_color: Color::WHITE, + unlit: true, + cull_mode: None, + base_color_texture: background_image.clone(), + alpha_mode: if blend_state.is_some() || *transparent { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, + ..default() + }, MaterialKey::Pbr { albedo, roughness, metallic, emissive, + .. } => { let base_color = Color::srgba( albedo[0] as f32 / 255.0, @@ -54,7 +71,7 @@ impl MaterialKey { albedo[2] as f32 / 255.0, albedo[3] as f32 / 255.0, ); - let mat = StandardMaterial { + StandardMaterial { base_color, unlit: false, cull_mode: None, @@ -67,18 +84,30 @@ impl MaterialKey { emissive[3] as f32 / 255.0, ), ..default() - }; - materials.add(mat).untyped() + } } - MaterialKey::Custom(_) => unreachable!(), + MaterialKey::Custom { .. } => unreachable!(), } } + + pub fn to_material( + &self, + materials: &mut ResMut>, + ) -> UntypedHandle { + let blend_state = self.blend_state(); + let base = self.to_standard_material(); + let extended = ProcessingExtendedMaterial { + base, + extension: ProcessingMaterial { blend_state }, + }; + materials.add(extended).untyped() + } } -pub fn add_standard_materials(mut commands: Commands, meshes: Query<(Entity, &UntypedMaterial)>) { +pub fn add_processing_materials(mut commands: Commands, meshes: Query<(Entity, &UntypedMaterial)>) { for (entity, handle) in meshes.iter() { let handle = handle.deref().clone(); - if let Ok(handle) = handle.try_typed::() { + if let Ok(handle) = handle.try_typed::() { commands.entity(entity).insert(MeshMaterial3d(handle)); } } diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index dd049d2..2aac577 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -9,9 +9,10 @@ use bevy::{ ecs::system::SystemParam, math::{Affine3A, Mat4, Vec4}, prelude::*, + render::render_resource::BlendState, }; use command::{CommandBuffer, DrawCommand}; -use material::MaterialKey; +use material::{MaterialKey, ProcessingExtendedMaterial}; use primitive::{StrokeConfig, TessellationMode, box_mesh, empty_mesh, sphere_mesh}; use transform::TransformStack; @@ -20,6 +21,8 @@ use crate::{ geometry::Geometry, gltf::GltfNodeTransform, image::Image, + material::ProcessingMaterial, + material::custom::CustomMaterial, render::{material::UntypedMaterial, primitive::rect}, }; @@ -37,7 +40,8 @@ pub struct TransientMeshes(Vec); pub struct RenderResources<'w, 's> { commands: Commands<'w, 's>, meshes: ResMut<'w, Assets>, - materials: ResMut<'w, Assets>, + materials: ResMut<'w, Assets>, + custom_materials: ResMut<'w, Assets>, } struct BatchState { @@ -69,6 +73,7 @@ pub struct RenderState { pub stroke_weight: f32, pub stroke_config: StrokeConfig, pub material_key: MaterialKey, + pub blend_state: Option, pub transform: TransformStack, } @@ -82,7 +87,9 @@ impl RenderState { material_key: MaterialKey::Color { transparent: false, background_image: None, + blend_state: None, }, + blend_state: None, transform: TransformStack::new(), } } @@ -95,7 +102,9 @@ impl RenderState { self.material_key = MaterialKey::Color { transparent: false, background_image: None, + blend_state: None, }; + self.blend_state = None; self.transform = TransformStack::new(); } @@ -175,12 +184,14 @@ pub fn flush_draw_commands( roughness: (r * 255.0) as u8, metallic, emissive, + blend_state: None, }, _ => MaterialKey::Pbr { albedo: [255, 255, 255, 255], roughness: (r * 255.0) as u8, metallic: 0, emissive: [0, 0, 0, 0], + blend_state: None, }, }; } @@ -196,12 +207,14 @@ pub fn flush_draw_commands( roughness, metallic: (m * 255.0) as u8, emissive, + blend_state: None, }, _ => MaterialKey::Pbr { albedo: [255, 255, 255, 255], roughness: 128, metallic: (m * 255.0) as u8, emissive: [0, 0, 0, 0], + blend_state: None, }, }; } @@ -218,12 +231,14 @@ pub fn flush_draw_commands( roughness, metallic, emissive: [r, g, b, a], + blend_state: None, }, _ => MaterialKey::Pbr { albedo: [255, 255, 255, 255], roughness: 128, metallic: 0, emissive: [r, g, b, a], + blend_state: None, }, }; } @@ -231,6 +246,7 @@ pub fn flush_draw_commands( state.material_key = MaterialKey::Color { transparent: state.fill_is_transparent(), background_image: None, + blend_state: None, }; } DrawCommand::Rect { x, y, w, h, radii } => { @@ -284,6 +300,7 @@ pub fn flush_draw_commands( let material_key = MaterialKey::Color { transparent: color.alpha() < 1.0, background_image: None, + blend_state: None, }; let material_handle = material_key.to_material(&mut res.materials); @@ -311,6 +328,7 @@ pub fn flush_draw_commands( let material_key = MaterialKey::Color { transparent: false, background_image: Some(p_image.handle.clone()), + blend_state: None, }; let material_handle = material_key.to_material(&mut res.materials); @@ -340,7 +358,7 @@ pub fn flush_draw_commands( let material_key = material_key_with_fill(&state); let material_handle = match &material_key { - MaterialKey::Custom(mat_entity) => { + MaterialKey::Custom { entity: mat_entity, .. } => { let Some(handle) = p_material_handles.get(*mat_entity).ok() else { warn!("Could not find material for entity {:?}", mat_entity); continue; @@ -374,8 +392,14 @@ pub fn flush_draw_commands( batch.draw_index += 1; } + DrawCommand::BlendMode(blend_state) => { + state.blend_state = blend_state; + } DrawCommand::Material(entity) => { - state.material_key = MaterialKey::Custom(entity); + state.material_key = MaterialKey::Custom { + entity, + blend_state: None, + }; } DrawCommand::Box { width, @@ -449,13 +473,30 @@ fn spawn_mesh( }; let material_handle = match key { - MaterialKey::Custom(entity) => match material_handles.get(*entity) { - Ok(handle) => handle.0.clone(), - Err(_) => { + MaterialKey::Custom { entity, blend_state } => { + let Some(untyped) = material_handles.get(*entity).ok() else { warn!("Custom material entity {:?} not found", entity); return; + }; + match *blend_state { + // No blend override — use the original handle + None => untyped.0.clone(), + // Blend override — clone the custom material with the blend state + Some(bs) => { + if let Ok(handle) = untyped.0.clone().try_typed::() { + if let Some(original) = res.custom_materials.get(&handle) { + let mut variant = original.clone(); + variant.blend_state = Some(bs); + res.custom_materials.add(variant).untyped() + } else { + untyped.0.clone() + } + } else { + untyped.0.clone() + } + } } - }, + } _ => key.to_material(&mut res.materials), }; @@ -471,7 +512,10 @@ fn spawn_mesh( fn needs_batch(batch: &BatchState, state: &RenderState, material_key: &MaterialKey) -> bool { let material_changed = batch.material_key.as_ref() != Some(material_key); let transform_changed = batch.transform != state.transform.current(); - material_changed || transform_changed + // When a custom blend mode is active, each shape needs its own draw call + // so that shapes composite against each other in draw order. + let requires_separate_draws = state.blend_state.is_some(); + material_changed || transform_changed || requires_separate_draws } fn start_batch( @@ -487,13 +531,18 @@ fn start_batch( batch.current_mesh = Some(empty_mesh()); } -fn material_key_with_color(key: &MaterialKey, color: Color) -> MaterialKey { +fn material_key_with_color( + key: &MaterialKey, + color: Color, + blend_state: Option, +) -> MaterialKey { match key { MaterialKey::Color { background_image, .. } => MaterialKey::Color { transparent: color.alpha() < 1.0, background_image: background_image.clone(), + blend_state, }, MaterialKey::Pbr { roughness, @@ -507,15 +556,19 @@ fn material_key_with_color(key: &MaterialKey, color: Color) -> MaterialKey { roughness: *roughness, metallic: *metallic, emissive: *emissive, + blend_state, } } - MaterialKey::Custom(e) => MaterialKey::Custom(*e), + MaterialKey::Custom { entity, .. } => MaterialKey::Custom { + entity: *entity, + blend_state, + }, } } fn material_key_with_fill(state: &RenderState) -> MaterialKey { let color = state.fill_color.unwrap_or(Color::WHITE); - material_key_with_color(&state.material_key, color) + material_key_with_color(&state.material_key, color, state.blend_state) } fn add_fill( @@ -528,7 +581,7 @@ fn add_fill( let Some(color) = state.fill_color else { return; }; - let material_key = material_key_with_color(&state.material_key, color); + let material_key = material_key_with_color(&state.material_key, color, state.blend_state); if needs_batch(batch, state, &material_key) { start_batch(res, batch, state, material_key, material_handles); @@ -550,7 +603,7 @@ fn add_stroke( return; }; let stroke_weight = state.stroke_weight; - let material_key = material_key_with_color(&state.material_key, color); + let material_key = material_key_with_color(&state.material_key, color, state.blend_state); if needs_batch(batch, state, &material_key) { start_batch(res, batch, state, material_key, material_handles); @@ -588,7 +641,7 @@ fn add_shape3d( let mesh_handle = res.meshes.add(mesh); let fill_color = state.fill_color.unwrap_or(Color::WHITE); let material_handle = match &state.material_key { - MaterialKey::Custom(entity) => match material_handles.get(*entity) { + MaterialKey::Custom { entity, .. } => match material_handles.get(*entity) { Ok(handle) => handle.0.clone(), Err(_) => { warn!("Custom material entity {:?} not found", entity); @@ -599,18 +652,24 @@ fn add_shape3d( // a base color in the material, so for simplicity we just create a new material here // that is unlit and uses the fill color as the base color MaterialKey::Color { transparent, .. } => { - let mat = StandardMaterial { + let base = StandardMaterial { base_color: fill_color, unlit: true, cull_mode: None, - alpha_mode: if *transparent { + alpha_mode: if state.blend_state.is_some() || *transparent { AlphaMode::Blend } else { AlphaMode::Opaque }, ..default() }; - res.materials.add(mat).untyped() + let extended = ProcessingExtendedMaterial { + base, + extension: ProcessingMaterial { + blend_state: state.blend_state, + }, + }; + res.materials.add(extended).untyped() } _ => { let key = material_key_with_fill(state); diff --git a/examples/blend_modes.rs b/examples/blend_modes.rs new file mode 100644 index 0000000..cb66a40 --- /dev/null +++ b/examples/blend_modes.rs @@ -0,0 +1,117 @@ +mod glfw; + +use glfw::{GlfwContext, Key}; +use processing::prelude::*; + +const MODES: &[BlendMode] = &[ + BlendMode::Blend, + BlendMode::Add, + BlendMode::Subtract, + BlendMode::Darkest, + BlendMode::Lightest, + BlendMode::Difference, + BlendMode::Exclusion, + BlendMode::Multiply, + BlendMode::Screen, + BlendMode::Replace, +]; + +fn main() { + match sketch() { + Ok(_) => exit(0).unwrap(), + Err(e) => { + eprintln!("Error: {e:?}"); + exit(1).unwrap(); + } + } +} + +fn sketch() -> error::Result<()> { + let width = 500; + let height = 500; + let mut glfw_ctx = GlfwContext::new(width, height)?; + init(Config::default())?; + + let surface = glfw_ctx.create_surface(width, height)?; + let graphics = graphics_create(surface, width, height, TextureFormat::Rgba16Float)?; + + let mut index: usize = 0; + + while glfw_ctx.poll_events() { + match glfw_ctx.last_key { + Some(Key::Right | Key::Space) => { + index = (index + 1) % MODES.len(); + eprintln!("{}", MODES[index].name()); + } + Some(Key::Left) => { + index = (index + MODES.len() - 1) % MODES.len(); + eprintln!("{}", MODES[index].name()); + } + _ => {} + } + + graphics_begin_draw(graphics)?; + + graphics_record_command( + graphics, + DrawCommand::BackgroundColor(bevy::color::Color::srgba(0.15, 0.15, 0.15, 1.0)), + )?; + graphics_record_command(graphics, DrawCommand::NoStroke)?; + graphics_record_command( + graphics, + DrawCommand::BlendMode(MODES[index].to_blend_state()), + )?; + + // Red + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgba(0.9, 0.2, 0.2, 0.75)), + )?; + graphics_record_command( + graphics, + DrawCommand::Rect { + x: 80.0, + y: 100.0, + w: 200.0, + h: 250.0, + radii: [0.0; 4], + }, + )?; + + // Green + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgba(0.2, 0.8, 0.2, 0.75)), + )?; + graphics_record_command( + graphics, + DrawCommand::Rect { + x: 180.0, + y: 80.0, + w: 200.0, + h: 250.0, + radii: [0.0; 4], + }, + )?; + + // Blue + graphics_record_command( + graphics, + DrawCommand::Fill(bevy::color::Color::srgba(0.2, 0.3, 0.9, 0.75)), + )?; + graphics_record_command( + graphics, + DrawCommand::Rect { + x: 130.0, + y: 200.0, + w: 200.0, + h: 200.0, + radii: [0.0; 4], + }, + )?; + + graphics_end_draw(graphics)?; + } + + Ok(()) +} diff --git a/src/prelude.rs b/src/prelude.rs index 6f327b4..a81cd6c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,7 +8,7 @@ pub use processing_midi::{ midi_connect, midi_disconnect, midi_list_ports, midi_play_notes, midi_refresh_ports, }; pub use processing_render::{ - render::command::{DrawCommand, StrokeCapMode, StrokeJoinMode}, + render::command::{BlendMode, DrawCommand, StrokeCapMode, StrokeJoinMode, custom_blend_state}, *, }; From 7f908d092d3dc3d9fcd4a137e6652b47cb2a5fc6 Mon Sep 17 00:00:00 2001 From: charlotte Date: Fri, 27 Mar 2026 17:16:51 -0700 Subject: [PATCH 2/4] Blend modes. --- crates/processing_pyo3/src/graphics.rs | 4 -- crates/processing_render/src/graphics.rs | 3 +- .../processing_render/src/material/custom.rs | 8 +-- .../processing_render/src/render/command.rs | 6 +- crates/processing_render/src/render/mod.rs | 70 +++++++++++-------- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 44e1cd2..50fa7fc 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -21,10 +21,6 @@ use pyo3::{ use crate::glfw::GlfwContext; use crate::math::{extract_vec2, extract_vec3, extract_vec4}; -// --------------------------------------------------------------------------- -// BlendMode -// --------------------------------------------------------------------------- - #[pyclass(name = "BlendMode")] #[derive(Clone)] pub struct PyBlendMode { diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 0497424..1418a78 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -132,8 +132,7 @@ impl ProcessingProjection { impl CameraProjection for ProcessingProjection { fn get_clip_from_view(&self) -> Mat4 { - // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] - // This is for interoperability with Bevy's reverse-Z depth pipeline. + // near/far swapped for Bevy's reverse-Z depth Mat4::orthographic_rh( 0.0, self.width, diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index 7d07bbf..aef8d5c 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -1,4 +1,4 @@ -use std::any::TypeId; +use std::any::{Any, TypeId}; use std::borrow::Cow; use std::sync::Arc; @@ -11,7 +11,7 @@ wesl::wesl_pkg!(lygia); use bevy::{ asset::{AsAssetId, AssetEventSystems}, - core_pipeline::core_3d::{Opaque3d, Transparent3d}, + core_pipeline::core_3d::Opaque3d, ecs::system::{ SystemParamItem, lifetimeless::{SRes, SResMut}, @@ -50,8 +50,6 @@ use bevy_naga_reflect::dynamic_shader::DynamicShader; use bevy::shader::Shader as ShaderAsset; -use std::any::Any; - use crate::material::MaterialValue; use crate::render::material::UntypedMaterial; use processing_core::config::{Config, ConfigKey}; @@ -86,7 +84,7 @@ pub struct CustomMaterial { pub shader_handle: Handle, pub has_vertex: bool, pub has_fragment: bool, - pub blend_state: Option, + pub blend_state: Option, } #[derive(Component)] diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 92cd957..9df30db 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -75,7 +75,6 @@ impl From for BlendMode { } } -/// Convert a u8 to a WebGPU BlendFactor. pub fn blend_factor_from_u8(v: u8) -> BlendFactor { match v { 0 => BlendFactor::Zero, @@ -93,7 +92,6 @@ pub fn blend_factor_from_u8(v: u8) -> BlendFactor { } } -/// Convert a u8 to a WebGPU BlendOperation. pub fn blend_op_from_u8(v: u8) -> BlendOperation { match v { 0 => BlendOperation::Add, @@ -105,7 +103,6 @@ pub fn blend_op_from_u8(v: u8) -> BlendOperation { } } -/// Build a BlendState from individual component parameters. pub fn custom_blend_state( color_src: u8, color_dst: u8, @@ -150,8 +147,7 @@ impl BlendMode { } } - /// Convert to a WebGPU BlendState, matching Processing's OpenGL blend configurations. - /// Returns None for the default Blend mode (standard alpha blending handled by AlphaMode). + /// Returns None for the default Blend mode, letting AlphaMode handle blending. pub fn to_blend_state(self) -> Option { use BlendFactor::*; use BlendOperation::*; diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 2aac577..e0b4c54 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -358,12 +358,16 @@ pub fn flush_draw_commands( let material_key = material_key_with_fill(&state); let material_handle = match &material_key { - MaterialKey::Custom { entity: mat_entity, .. } => { - let Some(handle) = p_material_handles.get(*mat_entity).ok() else { + MaterialKey::Custom { entity: mat_entity, blend_state } => { + let Some(untyped) = p_material_handles.get(*mat_entity).ok() else { warn!("Could not find material for entity {:?}", mat_entity); continue; }; - handle.0.clone() + clone_custom_material_with_blend( + &mut res.custom_materials, + &untyped.0, + *blend_state, + ) } _ => material_key.to_material(&mut res.materials), }; @@ -478,24 +482,11 @@ fn spawn_mesh( warn!("Custom material entity {:?} not found", entity); return; }; - match *blend_state { - // No blend override — use the original handle - None => untyped.0.clone(), - // Blend override — clone the custom material with the blend state - Some(bs) => { - if let Ok(handle) = untyped.0.clone().try_typed::() { - if let Some(original) = res.custom_materials.get(&handle) { - let mut variant = original.clone(); - variant.blend_state = Some(bs); - res.custom_materials.add(variant).untyped() - } else { - untyped.0.clone() - } - } else { - untyped.0.clone() - } - } - } + clone_custom_material_with_blend( + &mut res.custom_materials, + &untyped.0, + *blend_state, + ) } _ => key.to_material(&mut res.materials), }; @@ -512,8 +503,6 @@ fn spawn_mesh( fn needs_batch(batch: &BatchState, state: &RenderState, material_key: &MaterialKey) -> bool { let material_changed = batch.material_key.as_ref() != Some(material_key); let transform_changed = batch.transform != state.transform.current(); - // When a custom blend mode is active, each shape needs its own draw call - // so that shapes composite against each other in draw order. let requires_separate_draws = state.blend_state.is_some(); material_changed || transform_changed || requires_separate_draws } @@ -531,6 +520,27 @@ fn start_batch( batch.current_mesh = Some(empty_mesh()); } +fn clone_custom_material_with_blend( + custom_materials: &mut Assets, + original: &UntypedHandle, + blend_state: Option, +) -> UntypedHandle { + match blend_state { + None => original.clone(), + Some(bs) => { + let Ok(handle) = original.clone().try_typed::() else { + return original.clone(); + }; + let Some(original_mat) = custom_materials.get(&handle) else { + return original.clone(); + }; + let mut variant = original_mat.clone(); + variant.blend_state = Some(bs); + custom_materials.add(variant).untyped() + } + } +} + fn material_key_with_color( key: &MaterialKey, color: Color, @@ -641,13 +651,17 @@ fn add_shape3d( let mesh_handle = res.meshes.add(mesh); let fill_color = state.fill_color.unwrap_or(Color::WHITE); let material_handle = match &state.material_key { - MaterialKey::Custom { entity, .. } => match material_handles.get(*entity) { - Ok(handle) => handle.0.clone(), - Err(_) => { + MaterialKey::Custom { entity, .. } => { + let Some(untyped) = material_handles.get(*entity).ok() else { warn!("Custom material entity {:?} not found", entity); return; - } - }, + }; + clone_custom_material_with_blend( + &mut res.custom_materials, + &untyped.0, + state.blend_state, + ) + } // TODO: in 2d, we use vertex colors. `to_material` becomes complicated if we also encode // a base color in the material, so for simplicity we just create a new material here // that is unlit and uses the fill color as the base color From c3469aa5c0a6af9b01cc0c04b7bed40cfbb02044 Mon Sep 17 00:00:00 2001 From: charlotte Date: Mon, 30 Mar 2026 11:51:46 -0700 Subject: [PATCH 3/4] Implement blend mode support. --- crates/processing_ffi/src/lib.rs | 61 +++++++--- crates/processing_input/src/lib.rs | 9 ++ .../processing_pyo3/examples/blend_modes.py | 30 +++++ crates/processing_pyo3/src/graphics.rs | 60 +++++++--- crates/processing_pyo3/src/input.rs | 6 + crates/processing_pyo3/src/lib.rs | 42 +------ crates/processing_render/src/graphics.rs | 1 - .../processing_render/src/material/custom.rs | 4 +- crates/processing_render/src/material/mod.rs | 6 +- .../processing_render/src/render/command.rs | 107 ++++++++++-------- .../processing_render/src/render/material.rs | 2 +- crates/processing_render/src/render/mod.rs | 20 ++-- examples/blend_modes.rs | 22 ++-- 13 files changed, 220 insertions(+), 150 deletions(-) create mode 100644 crates/processing_pyo3/examples/blend_modes.py diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index bd45c7b..bd8aed5 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -466,24 +466,16 @@ pub extern "C" fn processing_shear_y(graphics_id: u64, angle: f32) { error::check(|| graphics_record_command(graphics_entity, DrawCommand::ShearY { angle })); } -/// Set the blend mode. -/// -/// Mode values: 0=BLEND, 1=ADD, 2=SUBTRACT, 3=DARKEST, 4=LIGHTEST, -/// 5=DIFFERENCE, 6=EXCLUSION, 7=MULTIPLY, 8=SCREEN, 9=REPLACE #[unsafe(no_mangle)] pub extern "C" fn processing_set_blend_mode(graphics_id: u64, mode: u8) { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - let blend_state = processing::prelude::BlendMode::from(mode).to_blend_state(); - error::check(|| graphics_record_command(graphics_entity, DrawCommand::BlendMode(blend_state))); + error::check(|| { + let blend_state = processing::prelude::BlendMode::try_from(mode)?.to_blend_state(); + graphics_record_command(graphics_entity, DrawCommand::BlendMode(blend_state)) + }); } -/// Set a custom blend mode by specifying individual blend components. -/// -/// Each factor/operation is a u8 mapping to the WebGPU BlendFactor/BlendOperation enums. -/// BlendFactor: 0=Zero, 1=One, 2=Src, 3=OneMinusSrc, 4=SrcAlpha, 5=OneMinusSrcAlpha, -/// 6=Dst, 7=OneMinusDst, 8=DstAlpha, 9=OneMinusDstAlpha, 10=SrcAlphaSaturated -/// BlendOperation: 0=Add, 1=Subtract, 2=ReverseSubtract, 3=Min, 4=Max #[unsafe(no_mangle)] pub extern "C" fn processing_set_custom_blend_mode( graphics_id: u64, @@ -496,10 +488,10 @@ pub extern "C" fn processing_set_custom_blend_mode( ) { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - let blend_state = custom_blend_state( - color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, - ); error::check(|| { + let blend_state = custom_blend_state( + color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, + )?; graphics_record_command(graphics_entity, DrawCommand::BlendMode(Some(blend_state))) }); } @@ -805,6 +797,35 @@ pub const PROCESSING_STROKE_JOIN_ROUND: u8 = 0; pub const PROCESSING_STROKE_JOIN_MITER: u8 = 1; pub const PROCESSING_STROKE_JOIN_BEVEL: u8 = 2; +pub const PROCESSING_BLEND_MODE_BLEND: u8 = 0; +pub const PROCESSING_BLEND_MODE_ADD: u8 = 1; +pub const PROCESSING_BLEND_MODE_SUBTRACT: u8 = 2; +pub const PROCESSING_BLEND_MODE_DARKEST: u8 = 3; +pub const PROCESSING_BLEND_MODE_LIGHTEST: u8 = 4; +pub const PROCESSING_BLEND_MODE_DIFFERENCE: u8 = 5; +pub const PROCESSING_BLEND_MODE_EXCLUSION: u8 = 6; +pub const PROCESSING_BLEND_MODE_MULTIPLY: u8 = 7; +pub const PROCESSING_BLEND_MODE_SCREEN: u8 = 8; +pub const PROCESSING_BLEND_MODE_REPLACE: u8 = 9; + +pub const PROCESSING_BLEND_FACTOR_ZERO: u8 = 0; +pub const PROCESSING_BLEND_FACTOR_ONE: u8 = 1; +pub const PROCESSING_BLEND_FACTOR_SRC: u8 = 2; +pub const PROCESSING_BLEND_FACTOR_ONE_MINUS_SRC: u8 = 3; +pub const PROCESSING_BLEND_FACTOR_SRC_ALPHA: u8 = 4; +pub const PROCESSING_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA: u8 = 5; +pub const PROCESSING_BLEND_FACTOR_DST: u8 = 6; +pub const PROCESSING_BLEND_FACTOR_ONE_MINUS_DST: u8 = 7; +pub const PROCESSING_BLEND_FACTOR_DST_ALPHA: u8 = 8; +pub const PROCESSING_BLEND_FACTOR_ONE_MINUS_DST_ALPHA: u8 = 9; +pub const PROCESSING_BLEND_FACTOR_SRC_ALPHA_SATURATED: u8 = 10; + +pub const PROCESSING_BLEND_OP_ADD: u8 = 0; +pub const PROCESSING_BLEND_OP_SUBTRACT: u8 = 1; +pub const PROCESSING_BLEND_OP_REVERSE_SUBTRACT: u8 = 2; +pub const PROCESSING_BLEND_OP_MIN: u8 = 3; +pub const PROCESSING_BLEND_OP_MAX: u8 = 4; + #[unsafe(no_mangle)] pub extern "C" fn processing_geometry_layout_create() -> u64 { error::clear_error(); @@ -1602,6 +1623,16 @@ pub extern "C" fn processing_key_is_down(key_code: u32) -> bool { .unwrap_or(false) } +#[unsafe(no_mangle)] +pub extern "C" fn processing_key_just_pressed(key_code: u32) -> bool { + error::clear_error(); + error::check(|| { + let kc = key_code_from_u32(key_code)?; + input_key_just_pressed(kc) + }) + .unwrap_or(false) +} + #[unsafe(no_mangle)] pub extern "C" fn processing_key() -> u32 { error::clear_error(); diff --git a/crates/processing_input/src/lib.rs b/crates/processing_input/src/lib.rs index 1148d4a..997a483 100644 --- a/crates/processing_input/src/lib.rs +++ b/crates/processing_input/src/lib.rs @@ -241,6 +241,15 @@ pub fn input_key_is_pressed() -> error::Result { }) } +pub fn input_key_just_pressed(key_code: KeyCode) -> error::Result { + app_mut(|app| { + Ok(app + .world() + .resource::>() + .just_pressed(key_code)) + }) +} + pub fn input_key_is_down(key_code: KeyCode) -> error::Result { app_mut(|app| { Ok(app diff --git a/crates/processing_pyo3/examples/blend_modes.py b/crates/processing_pyo3/examples/blend_modes.py new file mode 100644 index 0000000..fa367fa --- /dev/null +++ b/crates/processing_pyo3/examples/blend_modes.py @@ -0,0 +1,30 @@ +from mewnala import * + +MODES = [BLEND, ADD, SUBTRACT, DARKEST, LIGHTEST, DIFFERENCE, EXCLUSION, MULTIPLY, SCREEN, REPLACE] +index = 0 + +def setup(): + size(500, 500) + +def draw(): + global index + + if key_just_pressed(RIGHT_ARROW) or key_just_pressed(SPACE): + index = (index + 1) % len(MODES) + elif key_just_pressed(LEFT_ARROW): + index = (index - 1) % len(MODES) + + background(38) + no_stroke() + blend_mode(MODES[index]) + + fill(230, 51, 51, 191) + rect(80, 100, 200, 250) + + fill(51, 204, 51, 191) + rect(180, 80, 200, 250) + + fill(51, 77, 230, 191) + rect(130, 200, 200, 200) + +run() diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 50fa7fc..e2650ca 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1,10 +1,7 @@ use crate::color::{ColorMode, extract_color_with_mode}; -use crate::color::{ColorMode, extract_color_with_mode}; -use crate::glfw::GlfwContext; use crate::glfw::GlfwContext; use crate::input; use crate::math::{extract_vec2, extract_vec3, extract_vec4}; -use crate::math::{extract_vec2, extract_vec3, extract_vec4}; use bevy::{ color::{ColorToPacked, Srgba}, math::Vec4, @@ -18,9 +15,6 @@ use pyo3::{ types::{PyDict, PyTuple}, }; -use crate::glfw::GlfwContext; -use crate::math::{extract_vec2, extract_vec3, extract_vec4}; - #[pyclass(name = "BlendMode")] #[derive(Clone)] pub struct PyBlendMode { @@ -39,11 +33,6 @@ impl PyBlendMode { #[pymethods] impl PyBlendMode { - /// Create a custom blend mode by specifying individual blend components. - /// - /// All arguments are keyword-only. Use the blend factor constants (ZERO, ONE, - /// SRC_COLOR, SRC_ALPHA, DST_COLOR, etc.) and blend operation constants - /// (OP_ADD, OP_SUBTRACT, OP_REVERSE_SUBTRACT, OP_MIN, OP_MAX). #[new] #[pyo3(signature = (*, color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op))] fn new( @@ -53,13 +42,15 @@ impl PyBlendMode { alpha_src: u8, alpha_dst: u8, alpha_op: u8, - ) -> Self { - Self { - blend_state: Some(custom_blend_state( - color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, - )), + ) -> PyResult { + let blend_state = custom_blend_state( + color_src, color_dst, color_op, alpha_src, alpha_dst, alpha_op, + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(Self { + blend_state: Some(blend_state), name: None, - } + }) } fn __repr__(&self) -> String { @@ -68,8 +59,41 @@ impl PyBlendMode { None => "BlendMode(custom)".to_string(), } } -} + #[classattr] + const ZERO: u8 = 0; + #[classattr] + const ONE: u8 = 1; + #[classattr] + const SRC_COLOR: u8 = 2; + #[classattr] + const ONE_MINUS_SRC_COLOR: u8 = 3; + #[classattr] + const SRC_ALPHA: u8 = 4; + #[classattr] + const ONE_MINUS_SRC_ALPHA: u8 = 5; + #[classattr] + const DST_COLOR: u8 = 6; + #[classattr] + const ONE_MINUS_DST_COLOR: u8 = 7; + #[classattr] + const DST_ALPHA: u8 = 8; + #[classattr] + const ONE_MINUS_DST_ALPHA: u8 = 9; + #[classattr] + const SRC_ALPHA_SATURATED: u8 = 10; + + #[classattr] + const OP_ADD: u8 = 0; + #[classattr] + const OP_SUBTRACT: u8 = 1; + #[classattr] + const OP_REVERSE_SUBTRACT: u8 = 2; + #[classattr] + const OP_MIN: u8 = 3; + #[classattr] + const OP_MAX: u8 = 4; +} #[pyclass(unsendable)] pub struct Surface { diff --git a/crates/processing_pyo3/src/input.rs b/crates/processing_pyo3/src/input.rs index 2b9f258..fa9feb4 100644 --- a/crates/processing_pyo3/src/input.rs +++ b/crates/processing_pyo3/src/input.rs @@ -62,6 +62,12 @@ pub fn key_is_down(key_code: u32) -> PyResult { processing::prelude::input_key_is_down(kc).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } +pub fn key_just_pressed(key_code: u32) -> PyResult { + let kc = u32_to_key_code(key_code)?; + processing::prelude::input_key_just_pressed(kc) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) +} + pub fn key() -> PyResult> { processing::prelude::input_key() .map(|opt| opt.map(String::from)) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index ee0af25..54052f5 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -345,43 +345,6 @@ mod mewnala { #[pymodule_export] const XYZ: u8 = 9; - // Blend factor constants (for BlendMode custom constructor) - #[pymodule_export] - const ZERO: u8 = 0; - #[pymodule_export] - const ONE: u8 = 1; - #[pymodule_export] - const SRC_COLOR: u8 = 2; - #[pymodule_export] - const ONE_MINUS_SRC_COLOR: u8 = 3; - #[pymodule_export] - const SRC_ALPHA: u8 = 4; - #[pymodule_export] - const ONE_MINUS_SRC_ALPHA: u8 = 5; - #[pymodule_export] - const DST_COLOR: u8 = 6; - #[pymodule_export] - const ONE_MINUS_DST_COLOR: u8 = 7; - #[pymodule_export] - const DST_ALPHA: u8 = 8; - #[pymodule_export] - const ONE_MINUS_DST_ALPHA: u8 = 9; - #[pymodule_export] - const SRC_ALPHA_SATURATED: u8 = 10; - - // Blend operation constants (for BlendMode custom constructor) - #[pymodule_export] - const OP_ADD: u8 = 0; - #[pymodule_export] - const OP_SUBTRACT: u8 = 1; - #[pymodule_export] - const OP_REVERSE_SUBTRACT: u8 = 2; - #[pymodule_export] - const OP_MIN: u8 = 3; - #[pymodule_export] - const OP_MAX: u8 = 4; - - // Blend mode preset constants (added in pymodule_init) #[pymodule_init] fn init(module: &Bound<'_, PyModule>) -> PyResult<()> { use processing::prelude::BlendMode; @@ -1045,4 +1008,9 @@ mod mewnala { fn key_is_down(key_code: u32) -> PyResult { input::key_is_down(key_code) } + + #[pyfunction] + fn key_just_pressed(key_code: u32) -> PyResult { + input::key_just_pressed(key_code) + } } diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 1418a78..7877d16 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -132,7 +132,6 @@ impl ProcessingProjection { impl CameraProjection for ProcessingProjection { fn get_clip_from_view(&self) -> Mat4 { - // near/far swapped for Bevy's reverse-Z depth Mat4::orthographic_rh( 0.0, self.width, diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index aef8d5c..8d08c5f 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -60,7 +60,7 @@ struct CustomMaterialKey { blend_state: Option, } -fn custom_blend_specialize( +fn specialize( key: &dyn Any, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayoutRef, @@ -432,7 +432,7 @@ impl ErasedRenderAsset for CustomMaterial { base_specialize: Some(base_specialize), material_layout: Some(bind_group_layout), material_key: ErasedMaterialKey::new(CustomMaterialKey { blend_state }), - user_specialize: Some(custom_blend_specialize), + user_specialize: Some(specialize), alpha_mode: if blend_state.is_some() { AlphaMode::Blend } else { diff --git a/crates/processing_render/src/material/mod.rs b/crates/processing_render/src/material/mod.rs index 07d737a..459b0f3 100644 --- a/crates/processing_render/src/material/mod.rs +++ b/crates/processing_render/src/material/mod.rs @@ -11,7 +11,7 @@ use bevy::pbr::{ use bevy::prelude::*; use bevy::render::render_resource::{AsBindGroup, BlendState}; use bevy::shader::ShaderRef; -use processing_core::error::ProcessingError; +use processing_core::error::{self, ProcessingError}; pub struct ProcessingMaterialPlugin; @@ -70,7 +70,7 @@ pub fn set_property( material_handles: Query<&UntypedMaterial>, mut standard_materials: ResMut>, mut custom_materials: ResMut>, -) -> processing_core::error::Result<()> { +) -> error::Result<()> { let untyped = material_handles .get(entity) .map_err(|_| ProcessingError::MaterialNotFound)?; @@ -100,7 +100,7 @@ pub fn destroy( material_handles: Query<&UntypedMaterial>, mut standard_materials: ResMut>, mut custom_materials: ResMut>, -) -> processing_core::error::Result<()> { +) -> error::Result<()> { let untyped = material_handles .get(entity) .map_err(|_| ProcessingError::MaterialNotFound)?; diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 9df30db..e7a459f 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use bevy::render::render_resource::{BlendComponent, BlendFactor, BlendOperation, BlendState}; +use processing_core::error::{self, ProcessingError}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] @@ -57,49 +58,57 @@ pub enum BlendMode { Replace = 9, } -impl From for BlendMode { - fn from(v: u8) -> Self { +impl TryFrom for BlendMode { + type Error = ProcessingError; + + fn try_from(v: u8) -> std::result::Result { match v { - 0 => Self::Blend, - 1 => Self::Add, - 2 => Self::Subtract, - 3 => Self::Darkest, - 4 => Self::Lightest, - 5 => Self::Difference, - 6 => Self::Exclusion, - 7 => Self::Multiply, - 8 => Self::Screen, - 9 => Self::Replace, - _ => Self::default(), + 0 => Ok(Self::Blend), + 1 => Ok(Self::Add), + 2 => Ok(Self::Subtract), + 3 => Ok(Self::Darkest), + 4 => Ok(Self::Lightest), + 5 => Ok(Self::Difference), + 6 => Ok(Self::Exclusion), + 7 => Ok(Self::Multiply), + 8 => Ok(Self::Screen), + 9 => Ok(Self::Replace), + _ => Err(ProcessingError::InvalidArgument(format!( + "unknown blend mode: {v}" + ))), } } } -pub fn blend_factor_from_u8(v: u8) -> BlendFactor { +fn blend_factor_from_u8(v: u8) -> std::result::Result { match v { - 0 => BlendFactor::Zero, - 1 => BlendFactor::One, - 2 => BlendFactor::Src, - 3 => BlendFactor::OneMinusSrc, - 4 => BlendFactor::SrcAlpha, - 5 => BlendFactor::OneMinusSrcAlpha, - 6 => BlendFactor::Dst, - 7 => BlendFactor::OneMinusDst, - 8 => BlendFactor::DstAlpha, - 9 => BlendFactor::OneMinusDstAlpha, - 10 => BlendFactor::SrcAlphaSaturated, - _ => BlendFactor::One, + 0 => Ok(BlendFactor::Zero), + 1 => Ok(BlendFactor::One), + 2 => Ok(BlendFactor::Src), + 3 => Ok(BlendFactor::OneMinusSrc), + 4 => Ok(BlendFactor::SrcAlpha), + 5 => Ok(BlendFactor::OneMinusSrcAlpha), + 6 => Ok(BlendFactor::Dst), + 7 => Ok(BlendFactor::OneMinusDst), + 8 => Ok(BlendFactor::DstAlpha), + 9 => Ok(BlendFactor::OneMinusDstAlpha), + 10 => Ok(BlendFactor::SrcAlphaSaturated), + _ => Err(ProcessingError::InvalidArgument(format!( + "unknown blend factor: {v}" + ))), } } -pub fn blend_op_from_u8(v: u8) -> BlendOperation { +fn blend_op_from_u8(v: u8) -> std::result::Result { match v { - 0 => BlendOperation::Add, - 1 => BlendOperation::Subtract, - 2 => BlendOperation::ReverseSubtract, - 3 => BlendOperation::Min, - 4 => BlendOperation::Max, - _ => BlendOperation::Add, + 0 => Ok(BlendOperation::Add), + 1 => Ok(BlendOperation::Subtract), + 2 => Ok(BlendOperation::ReverseSubtract), + 3 => Ok(BlendOperation::Min), + 4 => Ok(BlendOperation::Max), + _ => Err(ProcessingError::InvalidArgument(format!( + "unknown blend operation: {v}" + ))), } } @@ -110,19 +119,19 @@ pub fn custom_blend_state( alpha_src: u8, alpha_dst: u8, alpha_op: u8, -) -> BlendState { - BlendState { +) -> error::Result { + Ok(BlendState { color: BlendComponent { - src_factor: blend_factor_from_u8(color_src), - dst_factor: blend_factor_from_u8(color_dst), - operation: blend_op_from_u8(color_op), + src_factor: blend_factor_from_u8(color_src)?, + dst_factor: blend_factor_from_u8(color_dst)?, + operation: blend_op_from_u8(color_op)?, }, alpha: BlendComponent { - src_factor: blend_factor_from_u8(alpha_src), - dst_factor: blend_factor_from_u8(alpha_dst), - operation: blend_op_from_u8(alpha_op), + src_factor: blend_factor_from_u8(alpha_src)?, + dst_factor: blend_factor_from_u8(alpha_dst)?, + operation: blend_op_from_u8(alpha_op)?, }, - } + }) } const ALPHA_ADDITIVE: BlendComponent = BlendComponent { @@ -147,7 +156,6 @@ impl BlendMode { } } - /// Returns None for the default Blend mode, letting AlphaMode handle blending. pub fn to_blend_state(self) -> Option { use BlendFactor::*; use BlendOperation::*; @@ -168,7 +176,6 @@ impl BlendMode { color: color(SrcAlpha, One, ReverseSubtract), alpha: ALPHA_ADDITIVE, }), - // Blend factors are ignored by Min/Max operations Self::Darkest => Some(BlendState { color: color(One, One, Min), alpha: ALPHA_ADDITIVE, @@ -177,8 +184,13 @@ impl BlendMode { color: color(One, One, Max), alpha: ALPHA_ADDITIVE, }), - // |src - dst| — not representable with fixed-function blending; - // reverse subtract is the same approximation Processing's OpenGL renderer uses. + // TODO: this is an approximation as we can't do abs difference in fixed function + // blending. this should probs be a fullscreen post-process effect instead. if we + // choose to add shader based blending, we should also consider adding more + // blend modes + // + // alternatively, we could express these shader based blend modes via a generic + // composite filter, which would more accurately reflect what is actually happening Self::Difference => Some(BlendState { color: color(One, One, ReverseSubtract), alpha: BlendComponent { @@ -187,7 +199,6 @@ impl BlendMode { operation: Max, }, }), - // src + dst - 2*src*dst = (1-dst)*src + (1-src)*dst Self::Exclusion => Some(BlendState { color: color(OneMinusDst, OneMinusSrc, Add), alpha: BlendComponent { @@ -196,12 +207,10 @@ impl BlendMode { operation: Add, }, }), - // src * dst (alpha-aware: falls back to dst when src is transparent) Self::Multiply => Some(BlendState { color: color(Dst, OneMinusSrcAlpha, Add), alpha: ALPHA_ADDITIVE, }), - // src + dst - src*dst = (1-dst)*src + dst Self::Screen => Some(BlendState { color: color(OneMinusDst, One, Add), alpha: ALPHA_ADDITIVE, diff --git a/crates/processing_render/src/render/material.rs b/crates/processing_render/src/render/material.rs index 8744a8f..3775f92 100644 --- a/crates/processing_render/src/render/material.rs +++ b/crates/processing_render/src/render/material.rs @@ -40,7 +40,7 @@ impl MaterialKey { } } - pub fn to_standard_material(&self) -> StandardMaterial { + fn to_standard_material(&self) -> StandardMaterial { match self { MaterialKey::Color { transparent, diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index e0b4c54..ee1be7b 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -300,7 +300,7 @@ pub fn flush_draw_commands( let material_key = MaterialKey::Color { transparent: color.alpha() < 1.0, background_image: None, - blend_state: None, + blend_state: Some(BlendState::REPLACE), }; let material_handle = material_key.to_material(&mut res.materials); @@ -328,7 +328,7 @@ pub fn flush_draw_commands( let material_key = MaterialKey::Color { transparent: false, background_image: Some(p_image.handle.clone()), - blend_state: None, + blend_state: Some(BlendState::REPLACE), }; let material_handle = material_key.to_material(&mut res.materials); @@ -358,7 +358,10 @@ pub fn flush_draw_commands( let material_key = material_key_with_fill(&state); let material_handle = match &material_key { - MaterialKey::Custom { entity: mat_entity, blend_state } => { + MaterialKey::Custom { + entity: mat_entity, + blend_state, + } => { let Some(untyped) = p_material_handles.get(*mat_entity).ok() else { warn!("Could not find material for entity {:?}", mat_entity); continue; @@ -477,16 +480,15 @@ fn spawn_mesh( }; let material_handle = match key { - MaterialKey::Custom { entity, blend_state } => { + MaterialKey::Custom { + entity, + blend_state, + } => { let Some(untyped) = material_handles.get(*entity).ok() else { warn!("Custom material entity {:?} not found", entity); return; }; - clone_custom_material_with_blend( - &mut res.custom_materials, - &untyped.0, - *blend_state, - ) + clone_custom_material_with_blend(&mut res.custom_materials, &untyped.0, *blend_state) } _ => key.to_material(&mut res.materials), }; diff --git a/examples/blend_modes.rs b/examples/blend_modes.rs index cb66a40..3cfa467 100644 --- a/examples/blend_modes.rs +++ b/examples/blend_modes.rs @@ -1,6 +1,5 @@ -mod glfw; +use processing_glfw::GlfwContext; -use glfw::{GlfwContext, Key}; use processing::prelude::*; const MODES: &[BlendMode] = &[ @@ -38,16 +37,12 @@ fn sketch() -> error::Result<()> { let mut index: usize = 0; while glfw_ctx.poll_events() { - match glfw_ctx.last_key { - Some(Key::Right | Key::Space) => { - index = (index + 1) % MODES.len(); - eprintln!("{}", MODES[index].name()); - } - Some(Key::Left) => { - index = (index + MODES.len() - 1) % MODES.len(); - eprintln!("{}", MODES[index].name()); - } - _ => {} + if input_key_just_pressed(KeyCode::ArrowRight)? || input_key_just_pressed(KeyCode::Space)? { + index = (index + 1) % MODES.len(); + eprintln!("{}", MODES[index].name()); + } else if input_key_just_pressed(KeyCode::ArrowLeft)? { + index = (index + MODES.len() - 1) % MODES.len(); + eprintln!("{}", MODES[index].name()); } graphics_begin_draw(graphics)?; @@ -62,7 +57,6 @@ fn sketch() -> error::Result<()> { DrawCommand::BlendMode(MODES[index].to_blend_state()), )?; - // Red graphics_record_command( graphics, DrawCommand::Fill(bevy::color::Color::srgba(0.9, 0.2, 0.2, 0.75)), @@ -78,7 +72,6 @@ fn sketch() -> error::Result<()> { }, )?; - // Green graphics_record_command( graphics, DrawCommand::Fill(bevy::color::Color::srgba(0.2, 0.8, 0.2, 0.75)), @@ -94,7 +87,6 @@ fn sketch() -> error::Result<()> { }, )?; - // Blue graphics_record_command( graphics, DrawCommand::Fill(bevy::color::Color::srgba(0.2, 0.3, 0.9, 0.75)), From 367223af2eb8d1295cf516515a7341d2ab6f9dae Mon Sep 17 00:00:00 2001 From: charlotte Date: Mon, 30 Mar 2026 12:29:42 -0700 Subject: [PATCH 4/4] Ci. --- crates/processing_ffi/src/color.rs | 2 +- crates/processing_pyo3/src/graphics.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/processing_ffi/src/color.rs b/crates/processing_ffi/src/color.rs index 59d8d78..cde66b5 100644 --- a/crates/processing_ffi/src/color.rs +++ b/crates/processing_ffi/src/color.rs @@ -1,4 +1,4 @@ -use bevy::color::{LinearRgba, Srgba}; +use bevy::color::LinearRgba; use processing::prelude::color::{ColorMode, ColorSpace}; /// A color with 4 float components and its color space. diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index e2650ca..7d2e004 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -15,7 +15,7 @@ use pyo3::{ types::{PyDict, PyTuple}, }; -#[pyclass(name = "BlendMode")] +#[pyclass(name = "BlendMode", from_py_object)] #[derive(Clone)] pub struct PyBlendMode { pub(crate) blend_state: Option,