feat(renderer): add bloom/tonemap pipelines and convert lighting to HDR output

- deferred_pipeline.rs: add bloom_bind_group_layout, create_bloom_downsample_pipeline (NO blend),
  create_bloom_upsample_pipeline (additive One+One blend), tonemap_bind_group_layout, create_tonemap_pipeline
- deferred_lighting.wgsl: remove Reinhard tonemap + gamma correction; output raw HDR linear colour
- lib.rs: export new pipeline functions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:49:32 +09:00
parent 6d30151be1
commit 4debec43e7
3 changed files with 291 additions and 7 deletions

View File

@@ -303,13 +303,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let ssgi_indirect = ssgi_data.gba;
let ambient = (diffuse_ibl + specular_ibl) * ao * ssgi_ao + ssgi_indirect;
var color = ambient + Lo;
// Reinhard tone mapping
color = color / (color + vec3<f32>(1.0));
// Gamma correction
color = pow(color, vec3<f32>(1.0 / 2.2));
// Output raw HDR linear colour; tonemap is applied in a separate tonemap pass.
let color = ambient + Lo;
return vec4<f32>(color, alpha);
}

View File

@@ -7,6 +7,9 @@ use crate::gbuffer::{
use crate::light::CameraUniform;
use crate::ssgi::{SsgiUniform, SSGI_OUTPUT_FORMAT};
use crate::rt_shadow::{RtShadowUniform, RT_SHADOW_FORMAT};
use crate::hdr::HDR_FORMAT;
use crate::bloom::BloomUniform;
use crate::tonemap::TonemapUniform;
/// Bind group layout for the G-Buffer pass camera uniform (dynamic offset, group 0).
pub fn gbuffer_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
@@ -616,3 +619,287 @@ pub fn create_rt_shadow_pipeline(
cache: None,
})
}
// ── Bloom pipelines ───────────────────────────────────────────────────────────
/// Bind group layout shared by both bloom pipelines (group 0):
/// binding 0 — input texture (filterable)
/// binding 1 — filtering sampler
/// binding 2 — BloomUniform buffer
pub fn bloom_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Bloom Bind Group Layout"),
entries: &[
// binding 0: input HDR texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: filtering sampler
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 2: BloomUniform buffer
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<BloomUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the bloom downsample render pipeline.
/// Entry point: `fs_downsample`. No blending — overwrites the mip target.
pub fn create_bloom_downsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Downsample Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Downsample Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_downsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
/// Create the bloom upsample render pipeline.
/// Entry point: `fs_upsample`. Additive blending (One + One) to accumulate mips.
pub fn create_bloom_upsample_pipeline(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Bloom Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom Upsample Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
let additive = 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,
},
};
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Bloom Upsample Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_upsample"),
targets: &[Some(wgpu::ColorTargetState {
format: HDR_FORMAT,
blend: Some(additive),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}
// ── Tonemap pipeline ──────────────────────────────────────────────────────────
/// Bind group layout for the tonemap pass (group 0):
/// binding 0 — HDR texture (filterable)
/// binding 1 — bloom texture (filterable)
/// binding 2 — filtering sampler
/// binding 3 — TonemapUniform buffer
pub fn tonemap_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Tonemap Bind Group Layout"),
entries: &[
// binding 0: HDR texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 1: bloom texture (filterable)
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
// binding 2: filtering sampler
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// binding 3: TonemapUniform buffer
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<TonemapUniform>() as u64,
),
},
count: None,
},
],
})
}
/// Create the tonemap render pipeline.
/// Writes to the surface swapchain format.
pub fn create_tonemap_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
layout: &wgpu::BindGroupLayout,
) -> wgpu::RenderPipeline {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Tonemap Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("tonemap_shader.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Tonemap Pipeline Layout"),
bind_group_layouts: &[layout],
immediate_size: 0,
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Tonemap Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[FullscreenVertex::LAYOUT],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview_mask: None,
cache: None,
})
}

View File

@@ -42,6 +42,8 @@ pub use deferred_pipeline::{
lighting_gbuffer_bind_group_layout, lighting_lights_bind_group_layout, lighting_shadow_bind_group_layout,
ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, create_ssgi_pipeline,
rt_shadow_gbuffer_bind_group_layout, rt_shadow_data_bind_group_layout, create_rt_shadow_pipeline,
bloom_bind_group_layout, create_bloom_downsample_pipeline, create_bloom_upsample_pipeline,
tonemap_bind_group_layout, create_tonemap_pipeline,
};
pub use ssgi::{SsgiResources, SsgiUniform, SSGI_OUTPUT_FORMAT};
pub use rt_accel::{RtAccel, RtInstance, BlasMeshData, mat4_to_tlas_transform};