Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion crates/lambda-rs-platform/src/wgpu/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<wgpu::BlendState> {
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 {
Expand Down Expand Up @@ -488,6 +534,7 @@ pub struct RenderPipelineBuilder<'a> {
vertex_buffers: Vec<VertexBufferLayoutDesc>,
cull_mode: CullingMode,
color_target_format: Option<wgpu::TextureFormat>,
blend_mode: BlendMode,
depth_stencil: Option<wgpu::DepthStencilState>,
sample_count: u32,
}
Expand All @@ -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,
};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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);
}
}
69 changes: 68 additions & 1 deletion crates/lambda-rs/src/render/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer>,
layout: VertexBufferLayout,
Expand Down Expand Up @@ -260,6 +292,7 @@ pub struct RenderPipelineBuilder {
immediate_data: Vec<std::ops::Range<u32>>,
bindings: Vec<BufferBinding>,
culling: CullingMode,
blend_mode: BlendMode,
bind_group_layouts: Vec<bind::BindGroupLayout>,
label: Option<String>,
use_depth: bool,
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<platform_pipeline::VertexAttributeDesc> = binding
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions demos/render/src/bin/reflective_room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use lambda::{
MeshBuilder,
},
pipeline::{
BlendMode,
CompareFunction,
CullingMode,
RenderPipelineBuilder,
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
1 change: 1 addition & 0 deletions docs/tutorials/rendering/techniques/reflective-room.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ImmediateData>() as u32)
.with_buffer(floor_vertex_buffer, floor_attributes)
.with_multi_sample(msaa_samples);
Expand Down
Loading