diff --git a/crates/lambda-rs-platform/src/wgpu/pipeline.rs b/crates/lambda-rs-platform/src/wgpu/pipeline.rs index 7c69f59e..2cae4bf8 100644 --- a/crates/lambda-rs-platform/src/wgpu/pipeline.rs +++ b/crates/lambda-rs-platform/src/wgpu/pipeline.rs @@ -75,6 +75,52 @@ impl CullingMode { } } +/// Blend mode for the (single) color target. +/// +/// Notes +/// - Most pipelines are opaque and should use `BlendMode::None` for best +/// performance, especially on tile-based GPUs. +/// - This is currently a single blend state for a single color attachment. +/// Per-attachment blending for MRT is future work. +#[derive(Clone, Copy, Debug)] +pub enum BlendMode { + /// No blending; replace destination (default). + None, + /// Standard alpha blending (`src_alpha`, `one_minus_src_alpha`). + AlphaBlending, + /// Premultiplied alpha blending. + PremultipliedAlpha, + /// Additive blending (`one`, `one`). + Additive, + /// Custom blend state. + Custom(wgpu::BlendState), +} + +impl BlendMode { + fn to_wgpu(self) -> Option { + return match self { + BlendMode::None => None, + BlendMode::AlphaBlending => Some(wgpu::BlendState::ALPHA_BLENDING), + BlendMode::PremultipliedAlpha => { + Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING) + } + BlendMode::Additive => Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + }), + BlendMode::Custom(state) => Some(state), + }; + } +} + /// Description of a single vertex attribute used by a pipeline. #[derive(Clone, Copy, Debug)] pub struct VertexAttributeDesc { @@ -488,6 +534,7 @@ pub struct RenderPipelineBuilder<'a> { vertex_buffers: Vec, cull_mode: CullingMode, color_target_format: Option, + blend_mode: BlendMode, depth_stencil: Option, sample_count: u32, } @@ -507,6 +554,7 @@ impl<'a> RenderPipelineBuilder<'a> { vertex_buffers: Vec::new(), cull_mode: CullingMode::Back, color_target_format: None, + blend_mode: BlendMode::None, depth_stencil: None, sample_count: 1, }; @@ -565,6 +613,12 @@ impl<'a> RenderPipelineBuilder<'a> { return self; } + /// Set the blend mode for the color target. Defaults to `BlendMode::None`. + pub fn with_blend(mut self, mode: BlendMode) -> Self { + self.blend_mode = mode; + return self; + } + /// Enable depth testing/writes using the provided depth format and default compare/write settings. /// /// Defaults: compare Less, depth writes enabled, no stencil. @@ -672,7 +726,7 @@ impl<'a> RenderPipelineBuilder<'a> { match &self.color_target_format { Some(fmt) => vec![Some(wgpu::ColorTargetState { format: *fmt, - blend: Some(wgpu::BlendState::ALPHA_BLENDING), + blend: self.blend_mode.to_wgpu(), write_mask: wgpu::ColorWrites::ALL, })], None => Vec::new(), @@ -755,4 +809,40 @@ mod tests { VertexStepMode::Vertex )); } + + #[test] + fn render_pipeline_builder_defaults_to_no_blending() { + let builder = RenderPipelineBuilder::new(); + assert!(matches!(builder.blend_mode, BlendMode::None)); + } + + #[test] + fn blend_mode_maps_to_wgpu_blend_state() { + assert_eq!( + BlendMode::AlphaBlending.to_wgpu(), + Some(wgpu::BlendState::ALPHA_BLENDING) + ); + assert_eq!( + BlendMode::PremultipliedAlpha.to_wgpu(), + Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING) + ); + assert_eq!( + BlendMode::Additive.to_wgpu(), + Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + }) + ); + let custom = wgpu::BlendState::ALPHA_BLENDING; + assert_eq!(BlendMode::Custom(custom).to_wgpu(), Some(custom)); + assert_eq!(BlendMode::None.to_wgpu(), None); + } } diff --git a/crates/lambda-rs/src/render/pipeline.rs b/crates/lambda-rs/src/render/pipeline.rs index 3bb7206b..caa40a73 100644 --- a/crates/lambda-rs/src/render/pipeline.rs +++ b/crates/lambda-rs/src/render/pipeline.rs @@ -119,6 +119,38 @@ impl RenderPipeline { /// though wgpu v28 immediates no longer use stage-scoped updates. pub use platform_pipeline::PipelineStage; +/// Blend mode for the pipeline's (single) color target. +/// +/// Notes +/// - Defaults to `BlendMode::None` (opaque/replace). Opt in to blending for +/// transparent geometry. +/// - This is currently a single blend state for a single color attachment. +/// Per-attachment blending for MRT is future work. +#[derive(Clone, Copy, Debug)] +pub enum BlendMode { + /// No blending; replace destination (default). + None, + /// Standard alpha blending. + AlphaBlending, + /// Premultiplied alpha blending. + PremultipliedAlpha, + /// Additive blending. + Additive, +} + +impl BlendMode { + fn to_platform(self) -> platform_pipeline::BlendMode { + return match self { + BlendMode::None => platform_pipeline::BlendMode::None, + BlendMode::AlphaBlending => platform_pipeline::BlendMode::AlphaBlending, + BlendMode::PremultipliedAlpha => { + platform_pipeline::BlendMode::PremultipliedAlpha + } + BlendMode::Additive => platform_pipeline::BlendMode::Additive, + }; + } +} + struct BufferBinding { buffer: Rc, layout: VertexBufferLayout, @@ -260,6 +292,7 @@ pub struct RenderPipelineBuilder { immediate_data: Vec>, bindings: Vec, culling: CullingMode, + blend_mode: BlendMode, bind_group_layouts: Vec, label: Option, use_depth: bool, @@ -283,6 +316,7 @@ impl RenderPipelineBuilder { immediate_data: Vec::new(), bindings: Vec::new(), culling: CullingMode::Back, + blend_mode: BlendMode::None, bind_group_layouts: Vec::new(), label: None, use_depth: false, @@ -372,6 +406,14 @@ impl RenderPipelineBuilder { return self; } + /// Configure blending for the pipeline's color target. + /// + /// Defaults to `BlendMode::None` (opaque). + pub fn with_blend(mut self, mode: BlendMode) -> Self { + self.blend_mode = mode; + return self; + } + /// Provide one or more bind group layouts used to create the pipeline layout. pub fn with_layouts(mut self, layouts: &[&bind::BindGroupLayout]) -> Self { self.bind_group_layouts = layouts.iter().map(|l| (*l).clone()).collect(); @@ -548,7 +590,8 @@ impl RenderPipelineBuilder { let mut rp_builder = platform_pipeline::RenderPipelineBuilder::new() .with_label(self.label.as_deref().unwrap_or("lambda-render-pipeline")) .with_layout(&pipeline_layout) - .with_cull_mode(self.culling.to_platform()); + .with_cull_mode(self.culling.to_platform()) + .with_blend(self.blend_mode.to_platform()); for binding in &self.bindings { let attributes: Vec = binding @@ -734,6 +777,30 @@ mod tests { )); } + /// Ensures blend modes default to `None` and map to platform blend modes. + #[test] + fn blend_mode_defaults_and_maps_to_platform() { + let builder = RenderPipelineBuilder::new(); + assert!(matches!(builder.blend_mode, BlendMode::None)); + + assert!(matches!( + BlendMode::None.to_platform(), + platform_pipeline::BlendMode::None + )); + assert!(matches!( + BlendMode::AlphaBlending.to_platform(), + platform_pipeline::BlendMode::AlphaBlending + )); + assert!(matches!( + BlendMode::PremultipliedAlpha.to_platform(), + platform_pipeline::BlendMode::PremultipliedAlpha + )); + assert!(matches!( + BlendMode::Additive.to_platform(), + platform_pipeline::BlendMode::Additive + )); + } + /// Ensures invalid MSAA sample counts are clamped/fallen back to `1`. #[test] fn pipeline_builder_invalid_sample_count_falls_back_to_one() { diff --git a/demos/render/src/bin/reflective_room.rs b/demos/render/src/bin/reflective_room.rs index acc2171c..e31ace8a 100644 --- a/demos/render/src/bin/reflective_room.rs +++ b/demos/render/src/bin/reflective_room.rs @@ -35,6 +35,7 @@ use lambda::{ MeshBuilder, }, pipeline::{ + BlendMode, CompareFunction, CullingMode, RenderPipelineBuilder, @@ -722,6 +723,7 @@ impl ReflectiveRoomExample { let mut floor_builder = RenderPipelineBuilder::new() .with_label("floor-visual") .with_culling(CullingMode::Back) + .with_blend(BlendMode::AlphaBlending) .with_immediate_data(immediate_data_size) .with_buffer( BufferBuilder::new() diff --git a/docs/rendering.md b/docs/rendering.md index 79445a13..71d801d2 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -172,9 +172,11 @@ let vbo = BufferBuilder::build_from_mesh(&mesh, &mut render_context) Create a pipeline, with optional push constants: ```rust -use lambda::render::pipeline::{RenderPipelineBuilder, PipelineStage}; +use lambda::render::pipeline::{BlendMode, RenderPipelineBuilder, PipelineStage}; let pipeline = RenderPipelineBuilder::new() + // Pipelines default to opaque (no blending). Opt in for transparent geometry: + // .with_blend(BlendMode::AlphaBlending) .with_push_constant(PipelineStage::VERTEX, 64) // size in bytes .with_buffer(vbo, mesh.attributes().to_vec()) .build(&mut render_context, &pass, &vs, Some(&fs)); diff --git a/docs/tutorials/rendering/techniques/reflective-room.md b/docs/tutorials/rendering/techniques/reflective-room.md index 088fcca1..9ab0fbc0 100644 --- a/docs/tutorials/rendering/techniques/reflective-room.md +++ b/docs/tutorials/rendering/techniques/reflective-room.md @@ -300,6 +300,7 @@ Draw the floor surface with a translucent tint so the reflection remains visible ```rust let mut floor_vis = RenderPipelineBuilder::new() .with_label("floor-visual") + .with_blend(lambda::render::pipeline::BlendMode::AlphaBlending) .with_immediate_data(std::mem::size_of::() as u32) .with_buffer(floor_vertex_buffer, floor_attributes) .with_multi_sample(msaa_samples);